Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use AndroidView overload which re-uses MapView to improve performance in lazy layouts #436

Merged
Merged
Show file tree
Hide file tree
Changes from 115 commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
1421dc7
Merge pull request #1 from googlemaps/main
philip-segerfast Oct 5, 2023
4279199
Modify GoogleMap to use AndroidView overload with onReset lambda
philip-segerfast Oct 13, 2023
284f93a
Update androidx.compose.ui:ui library to fix AndroidView bug
philip-segerfast Oct 13, 2023
6968d1e
Add MapsInLazyColumnActivity
philip-segerfast Oct 13, 2023
edc75c4
Hoist list items state
philip-segerfast Oct 13, 2023
7c62ce7
Bump compose-ui dependency
philip-segerfast Oct 28, 2023
c1f13b0
Update libs.versions.toml
philip-segerfast Oct 28, 2023
6b85dab
Show list of countries in MapsInLazyColumnActivity
philip-segerfast Oct 29, 2023
9c4dd29
Cleanup
philip-segerfast Oct 29, 2023
5a4073b
Merge branch 'main' into android-view-overload-re-use-map-view
philip-segerfast Oct 29, 2023
32e3230
Merge branch 'main' into android-view-overload-re-use-map-view
philip-segerfast Oct 29, 2023
7de5d02
Merge branch 'googlemaps:main' into main
philip-segerfast Jan 4, 2024
2f5f0b6
Remove compose-ui dependency version override
philip-segerfast Feb 1, 2024
e012317
Merge branch 'googlemaps:main' into main
philip-segerfast Feb 1, 2024
f38a3a1
Update libs.versions.toml
philip-segerfast Feb 1, 2024
44c524f
Add parameter to GoogleMap to allow user to re-use the underlying Map…
philip-segerfast Feb 1, 2024
17fbec4
Merge branch 'main' into android-view-overload-re-use-map-view
philip-segerfast Feb 1, 2024
5be5a18
Update libs.versions.toml
philip-segerfast Feb 1, 2024
3b1f79e
Update MapsInLazyColumnActivity.kt
philip-segerfast Feb 1, 2024
eb41c72
Fix merge issues
philip-segerfast Feb 1, 2024
c82853c
Refactor the GoogleMap composable to support reuse of underlying map+…
philip-segerfast Feb 4, 2024
267e62b
Replace ComposeNode usages with ReusableComposeNode
philip-segerfast Feb 6, 2024
706fe3a
Refactor GoogleMap to save Composition + MapClickListeners + other re…
philip-segerfast Feb 6, 2024
1042762
Add compose material library to build.gradle temporarily for demo pur…
philip-segerfast Feb 6, 2024
01dc74d
Implement ComposeNodeLifecycleCallback for MapClickListenerNode + set…
philip-segerfast Feb 7, 2024
4a3130d
Update GoogleMap.kt
philip-segerfast Feb 7, 2024
56971f1
Update MapsInLazyColumnActivity.kt
philip-segerfast Feb 7, 2024
7c7ed3a
Update ids.xml
philip-segerfast Feb 7, 2024
07ceeed
Update GoogleMap.kt
philip-segerfast Feb 7, 2024
19060db
Update MapClickListeners.kt
philip-segerfast Feb 7, 2024
2d9a158
Update GoogleMap.kt
philip-segerfast Feb 7, 2024
51f16d1
Make so that MapView lifecycle state "moves" through lifecycle states…
philip-segerfast Feb 21, 2024
86e940c
Move MapViewLifecycleController into its own file
philip-segerfast Feb 21, 2024
e0dc55c
Update MapViewLifecycleController.kt
philip-segerfast Feb 21, 2024
59be315
Remove composition reuse logic as it's moved to another PR
philip-segerfast Feb 22, 2024
fcdc833
Simplify MapViewLifecycleController logic
philip-segerfast Feb 22, 2024
1a7ca37
Re-register componentCallbacks if context has changed
philip-segerfast Feb 26, 2024
bc7de0e
Store map tag data in default tag instead of in separate tags by reso…
philip-segerfast Feb 26, 2024
9ac909e
Use CoroutineStart.UNDISPATCHED for creating composition
philip-segerfast Feb 26, 2024
6d6095a
Don't remove lifecycle from MapViewLifecycleController on detach to o…
philip-segerfast Feb 26, 2024
66a71cd
Reformat code
philip-segerfast Feb 26, 2024
3224ef5
Merge branch 'googlemaps:main' into main
philip-segerfast Feb 26, 2024
cfc5c66
Merge branch 'googlemaps:main' into android-view-overload-re-use-map-…
philip-segerfast Feb 26, 2024
72c745c
Move disposingComposition function to StreetView.kt as it's no longer…
philip-segerfast Apr 16, 2024
8c2991d
Use awaitCancellation instead of infinite delay
philip-segerfast Apr 16, 2024
49c9de7
Fix lifecycle event error message
philip-segerfast Apr 16, 2024
a8725b9
Use else branch instead of ON_ANY in nextLifecycleEvent
philip-segerfast Apr 16, 2024
a554a01
Change name of setCompositionAsync to launchComposition and make it m…
philip-segerfast Apr 16, 2024
6852c14
Merge branch 'main' into android-view-overload-re-use-map-view
philip-segerfast Apr 16, 2024
4789488
Undo import changes
philip-segerfast Apr 17, 2024
2077915
Undo unrelated import changes
philip-segerfast Apr 17, 2024
13247ce
Undo unrelated refactoring
philip-segerfast Apr 17, 2024
756ee07
Clarify that dependency is temporary
philip-segerfast Apr 18, 2024
bfe1e82
Create IncrementalLifecycleApplier
philip-segerfast Apr 18, 2024
61335fa
Delete MapViewLifecycleController and replace with IncrementalLifecyc…
philip-segerfast Apr 18, 2024
0cb4a5d
Resolve requirements
philip-segerfast Apr 18, 2024
3c5acc9
Add some logs for IncrementalLifecycleApplier
philip-segerfast Apr 18, 2024
c2a0f73
Remove `currentLifecycleState` parameter for IncrementalLifecycleApplier
philip-segerfast Apr 18, 2024
aa7a176
Make some functions in IncrementalLifecycleApplier pure and move to c…
philip-segerfast Apr 18, 2024
c67f1ed
Add support to override lifecycle state
philip-segerfast Apr 18, 2024
7ee881c
Remove LifecycleApplier stuff from GoogleMap
philip-segerfast Apr 19, 2024
133e751
Add show/hide button on MapsInLazyColumnActivity
philip-segerfast Apr 19, 2024
b253baf
Use LifecycleRegistry solution instead of custom solution
philip-segerfast Apr 19, 2024
e6210c1
Delete IncrementalLifecycleApplier.kt
philip-segerfast Apr 19, 2024
73abb30
Merge branch 'googlemaps:main' into main
philip-segerfast Apr 19, 2024
955cb4c
Update GoogleMap.kt
philip-segerfast Apr 22, 2024
79f6f93
Move between lifecycle states using custom approach inspired by Lifec…
philip-segerfast Apr 22, 2024
dd91206
Merge branch 'googlemaps:main' into android-view-overload-re-use-map-…
philip-segerfast Apr 22, 2024
93f709a
Move LifecycleEventObserver into its own class
philip-segerfast Apr 23, 2024
c58f1be
Rename moveForward/Backward to moveUp/Down
philip-segerfast Apr 23, 2024
863b994
Make moveToLifecycleState more clear
philip-segerfast Apr 23, 2024
c70638c
Make lifecycleOwner nullable and nullify on detach
philip-segerfast Apr 23, 2024
f485e21
Rename attachStateListener to onAttachStateListener for clarity
philip-segerfast Apr 23, 2024
d194481
Add moveToBaseState method to avoid reaching Lifecycle.State.CREATED …
philip-segerfast Apr 23, 2024
62b8660
Replace mapView.tag onRelease lambda with lifecycleObserver reference…
philip-segerfast Apr 23, 2024
3efa55e
Remove @Synchronized annotation
philip-segerfast Apr 23, 2024
5e58385
Make tagData() method a bit more clear
philip-segerfast Apr 23, 2024
801b9ee
Remove LocalContext.current + mapViewContext
philip-segerfast Apr 24, 2024
2e12e18
Make MapTagData + MapLifecycleEventObserver private
philip-segerfast Apr 24, 2024
99c0466
Update GoogleMap.kt
philip-segerfast Apr 24, 2024
e7d6dd4
Merge componentCallbacks into registerAndSaveNewComponentCallbacks
philip-segerfast Apr 24, 2024
91c1065
inline MapView.createComposition (move code to call site)
philip-segerfast Apr 24, 2024
43a4be8
Inline unregisterLifecycleObserver
philip-segerfast Apr 24, 2024
b3d0c45
Replace `isCompositionSet` with nullable `subcompositionJob`
philip-segerfast Apr 24, 2024
4ab3bc1
Rename `mapUpdaterScope` to `parentCompositionScope`
philip-segerfast Apr 24, 2024
b9b9d6a
Make MapTagData fully immutable + remove all logs + some related refa…
philip-segerfast Apr 24, 2024
3615f29
Remove debugging stuff
philip-segerfast Apr 24, 2024
1193cbf
Add some comments in AndroidView->factory
philip-segerfast Apr 24, 2024
899fe20
Remove "Create Composition" comment
philip-segerfast Apr 24, 2024
e0d725c
Remove compose material dependency for maps-compose module
philip-segerfast Apr 24, 2024
b1dd848
Some improvements to MapsInLazyColumnActivity + add more debugging co…
philip-segerfast Apr 24, 2024
a07f093
Convert tagData() to extension property
philip-segerfast Apr 24, 2024
dd6c0fe
Merge branch 'android-view-overload-re-use-map-view'
philip-segerfast Apr 24, 2024
f239db0
Move `composition` inside `try` block
philip-segerfast Apr 24, 2024
f076922
Formatting
philip-segerfast Apr 24, 2024
137746f
Add comment on CoroutineStart.UNDISPATCHED
philip-segerfast Apr 24, 2024
2d26a6a
Add KDoc to MapTagData
philip-segerfast Apr 24, 2024
bf4289f
Remove Suppress annotation
philip-segerfast Apr 24, 2024
34b1044
Rename launchComposition to launchSubcomposition
philip-segerfast Apr 24, 2024
14a5964
Remove obvious comments
philip-segerfast Apr 24, 2024
c562f3b
Move `lifecycleOwner` inside `object : View.OnAttachStateChangeListener`
philip-segerfast Apr 24, 2024
3bbb774
Fix comment
philip-segerfast Apr 24, 2024
bbc6ac1
Convert registerComponentCallbacks to a top-level class
philip-segerfast Apr 24, 2024
4af88f9
Inline componentCallbacks class
philip-segerfast Apr 24, 2024
348658f
Store reference to `Lifecycle` instead of `LifecycleOwner`
philip-segerfast Apr 24, 2024
a4cdd8b
Update comment
philip-segerfast Apr 24, 2024
b26f55d
Fix typo
philip-segerfast Apr 24, 2024
2e86585
Merge branch 'googlemaps:main' into android-view-overload-re-use-map-…
philip-segerfast Apr 25, 2024
748df47
Merge branch 'googlemaps:main' into android-view-overload-re-use-map-…
philip-segerfast May 3, 2024
653c857
Remove unused TAG constant
philip-segerfast May 7, 2024
0a3a0e5
Demo - Set buildingFocused state on initial composition
philip-segerfast May 9, 2024
99810a7
Make maps in MapsInLazyColumnActivity pannable
philip-segerfast May 9, 2024
14ea4c7
Merge branch 'main' into android-view-overload-re-use-map-view
philip-segerfast May 9, 2024
b4f5c29
Incorporate changes from #522 + remove MapClickListeners property fro…
philip-segerfast May 9, 2024
e519915
Merge branch 'main' into android-view-overload-re-use-map-view
philip-segerfast May 9, 2024
cff3b49
Revert MapClickListeners change + fix parameter order convention
philip-segerfast May 10, 2024
a917ed0
Use `mapUpdaterState.cameraPositionState` instead of `currentCameraPo…
philip-segerfast May 10, 2024
bec0b56
Merge branch 'merge-official-main' into android-view-overload-re-use-…
philip-segerfast Jul 4, 2024
e8bf202
Minor fixes
philip-segerfast Jul 4, 2024
894122b
Update app/src/main/AndroidManifest.xml
philip-segerfast Jul 9, 2024
c0357a7
Merge remote-tracking branch 'upstream/main' into merge-upstream
philip-segerfast Jul 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
<activity
android:name=".MapInColumnActivity"
android:exported="false"/>
<activity
android:name=".MapsInLazyColumnActivity"
android:exported="false"/>
<activity
android:name=".MarkerClusteringActivity"
philip-segerfast marked this conversation as resolved.
Show resolved Hide resolved
android:exported="false"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ class MainActivity : ComponentActivity() {
}) {
Text(getString(R.string.recomposition_activity))
}
Spacer(modifier = Modifier.padding(5.dp))
Button(
onClick = {
context.startActivity(Intent(context, MapsInLazyColumnActivity::class.java))
}) {
Text(getString(R.string.maps_in_lazy_column_activity))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package com.google.maps.android.compose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.IndoorBuilding
import com.google.android.gms.maps.model.LatLng

private data class CountryLocation(val name: String, val latLng: LatLng, val zoom: Float)

private typealias MapItemId = String

// From https://developers.google.com/public-data/docs/canonical/countries_csv
private val countries = listOf(
CountryLocation("Hong Kong", LatLng(22.396428, 114.109497), 5f),
CountryLocation("Madison Square Garden (has indoor mode)", LatLng(40.7504656, -73.9937246), 19.33f),
CountryLocation("Bolivia", LatLng(-16.290154, -63.588653), 5f),
CountryLocation("Ecuador", LatLng(-1.831239, -78.183406), 5f),
CountryLocation("Sweden", LatLng(60.128161, 18.643501), 5f),
CountryLocation("Eritrea", LatLng(15.179384, 39.782334), 5f),
CountryLocation("Portugal", LatLng(39.399872, -8.224454), 5f),
CountryLocation("Belgium", LatLng(50.503887, 4.469936), 5f),
CountryLocation("Slovakia", LatLng(48.669026, 19.699024), 5f),
CountryLocation("El Salvador", LatLng(13.794185, -88.89653), 5f),
CountryLocation("Bhutan", LatLng(27.514162, 90.433601), 5f),
CountryLocation("Saint Lucia", LatLng(13.909444, -60.978893), 5f),
CountryLocation("Uganda", LatLng(1.373333, 32.290275), 5f),
CountryLocation("South Africa", LatLng(-30.559482, 22.937506), 5f),
CountryLocation("Spain", LatLng(40.463667, -3.74922), 5f),
CountryLocation("Georgia", LatLng(42.315407, 43.356892), 5f),
CountryLocation("Burundi", LatLng(-3.373056, 29.918886), 5f)
)

private data class MapListItem(
val title: String,
val location: LatLng,
val zoom: Float,
val id: MapItemId
)

private val allItems = countries.mapIndexed { index, country ->
MapListItem(country.name, country.latLng, country.zoom, "MapInLazyColumn#$index")
}

class MapsInLazyColumnActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
var showLazyColumn by rememberSaveable { mutableStateOf(true) }
var visibleItems by rememberSaveable { mutableStateOf(allItems) }

fun setItemCount(count: Int) {
visibleItems = allItems.take(count.coerceIn(0, allItems.size))
}

Column {
Row(
Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.SpaceEvenly
) {
TextButton(onClick = { setItemCount(0) }) {
Text(text = "Clear")
}
TextButton(onClick = { setItemCount(visibleItems.size - 1) }) {
Text(text = "Remove")
}
TextButton(onClick = { showLazyColumn = !showLazyColumn }) {
Text(text = if (showLazyColumn) "Hide" else "Show")
}
TextButton(onClick = { setItemCount(visibleItems.size + 1) }) {
Text(text = "Add")
}
TextButton(onClick = { setItemCount(allItems.size) }) {
Text(text = "Fill")
}
}
if (showLazyColumn) {
Box(Modifier.border(1.dp, Color.LightGray.copy(0.5f))) {
MapsInLazyColumn(visibleItems)
}
}
}
}
}
}

@Composable
private fun MapsInLazyColumn(mapItems: List<MapListItem>) {
val lazyListState = rememberLazyListState()

val cameraPositionStates = mapItems.associate { item ->
item.id to rememberCameraPositionState(
key = item.id,
init = { position = CameraPosition.fromLatLngZoom(item.location, item.zoom) }
)
}
val visibleItemIds by remember(lazyListState) {
derivedStateOf {
lazyListState.layoutInfo.visibleItemsInfo.map { it.key as MapItemId }
}
}
val anyMapMoving by remember(cameraPositionStates) {
derivedStateOf {
visibleItemIds.any { cameraPositionStates[it]?.isMoving == true }
}
}

Box {
LazyColumn(
state = lazyListState,
userScrollEnabled = !anyMapMoving
) {
items(mapItems, key = { it.id }) { item ->
val cameraPositionState = cameraPositionStates[item.id]!!

Box(
Modifier
.fillMaxWidth()
.height(300.dp),
contentAlignment = Alignment.Center
) {
MapCard(item, cameraPositionState)
}
}
}
}
}

@OptIn(MapsComposeExperimentalApi::class)
@Composable
private fun MapCard(item: MapListItem, cameraPositionState: CameraPositionState) {
Card(
Modifier.padding(16.dp),
elevation = 4.dp
) {
var mapLoaded by remember { mutableStateOf(false) }
var buildingFocused: Boolean? by remember { mutableStateOf(null) }
var focusedBuildingInvocationCount by remember { mutableIntStateOf(0) }
var activatedIndoorLevel: String? by remember { mutableStateOf(null) }
var activatedIndoorLevelInvocationCount by remember { mutableIntStateOf(0) }
var onMapClickCount by remember { mutableIntStateOf(0) }

var map: GoogleMap? by remember { mutableStateOf(null) }

fun updateIndoorLevel() {
activatedIndoorLevel = map!!.focusedBuilding?.run { levels.getOrNull(activeLevelIndex)?.name }
}

Box {
GoogleMap(
onMapClick = {
onMapClickCount++
},
properties = remember {
MapProperties(
isBuildingEnabled = true,
isIndoorEnabled = true
)
},
cameraPositionState = cameraPositionState,
onMapLoaded = { mapLoaded = true },
indoorStateChangeListener = object : IndoorStateChangeListener {
override fun onIndoorBuildingFocused() {
super.onIndoorBuildingFocused()
focusedBuildingInvocationCount++
buildingFocused = (map!!.focusedBuilding != null)
updateIndoorLevel()
}

override fun onIndoorLevelActivated(building: IndoorBuilding) {
super.onIndoorLevelActivated(building)
activatedIndoorLevelInvocationCount++
updateIndoorLevel()
}
}
) {
MapEffect(Unit) { googleMap ->
map = googleMap
updateIndoorLevel()
buildingFocused = (googleMap.focusedBuilding != null)
}
}

AnimatedVisibility(!mapLoaded, enter = fadeIn(), exit = fadeOut()) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}

@Composable
fun TextWithBackground(text: String, fontWeight: FontWeight = FontWeight.Medium) {
Text(
modifier = Modifier.background(Color.White.copy(0.7f)),
text = text,
fontWeight = fontWeight,
fontSize = 10.sp
)
}

Column(
modifier = Modifier.align(Alignment.BottomStart)
) {
TextWithBackground(item.title, fontWeight = FontWeight.Bold)
TextWithBackground("Map loaded: $mapLoaded")
TextWithBackground("Map click count: $onMapClickCount")
TextWithBackground("Building focused: $buildingFocused")
TextWithBackground("Building focused invocation count: $focusedBuildingInvocationCount")
TextWithBackground("Indoor level: $activatedIndoorLevel")
TextWithBackground("Indoor level invocation count: $activatedIndoorLevelInvocationCount")
}
}
}
}
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@
<string name="street_view">Street View</string>
<string name="custom_location_button">Custom Location Button</string>
<string name="accessibility_button">Accessibility</string>
<string name="maps_in_lazy_column_activity">Maps in LazyColumn</string>
</resources>
Loading