From 97fcfa4a41fecf51016a4ea959c809ff46733961 Mon Sep 17 00:00:00 2001 From: Uli Bubenheimer Date: Mon, 29 Jan 2024 02:28:12 -0500 Subject: [PATCH 1/6] fix: improve MarkerState API This is a non-breaking change following suggestions from @arriolac for addressing #149: https://github.com/googlemaps/android-maps-compose/pull/150#discussion_r1016963262 This PR does not add an `onDrag` callback parameter to `Marker()`, which would be a somewhat breaking change; this functionality is not strictly necessary and I see alternatives that may be preferable. Summary of changes: 1. Deprecate MarkerState.dragState and DragState enum. These were carried over from GoogleMap SDK; they are events that were mischaracterized as states. 2. Replace with MarkerState.isDragging boolean. 3. Clarify KDoc in several places. 4. Add several examples providing patterns for common use cases. 5. Organize Marker-related examples into their own folder. Fixes #149 --- app/src/main/AndroidManifest.xml | 16 +- .../maps/android/compose/BasicMapActivity.kt | 4 +- .../maps/android/compose/MainActivity.kt | 34 ++++ .../{ => marker}/AdvancedMarkersActivity.kt | 8 +- .../{ => marker}/MarkerClusteringActivity.kt | 9 +- .../MarkerDragEventsActivity.kt | 127 +++++++++++++ .../MarkersCollectionActivity.kt | 154 ++++++++++++++++ ...ingDraggableMarkerWithDataModelActivity.kt | 168 ++++++++++++++++++ ...datingNoDragMarkerWithDataModelActivity.kt | 154 ++++++++++++++++ app/src/main/res/values/strings.xml | 4 + .../google/maps/android/compose/MapApplier.kt | 53 ++++-- .../com/google/maps/android/compose/Marker.kt | 55 +++++- 12 files changed, 764 insertions(+), 22 deletions(-) rename app/src/main/java/com/google/maps/android/compose/{ => marker}/AdvancedMarkersActivity.kt (94%) rename app/src/main/java/com/google/maps/android/compose/{ => marker}/MarkerClusteringActivity.kt (95%) create mode 100644 app/src/main/java/com/google/maps/android/compose/marker/markerdragevents/MarkerDragEventsActivity.kt create mode 100644 app/src/main/java/com/google/maps/android/compose/marker/markerscollection/MarkersCollectionActivity.kt create mode 100644 app/src/main/java/com/google/maps/android/compose/marker/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt create mode 100644 app/src/main/java/com/google/maps/android/compose/marker/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 146e0c53..89a741d4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,13 +43,13 @@ android:name=".BasicMapActivity" android:exported="false" /> + + + + diff --git a/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt b/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt index 79cb11c5..47eb2515 100644 --- a/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt @@ -141,7 +141,7 @@ fun GoogleMapView( val singapore4State = rememberMarkerState(position = singapore4) var circleCenter by remember { mutableStateOf(singapore) } - if (singaporeState.dragState == DragState.END) { + if (!singaporeState.isDragging) { circleCenter = singaporeState.position } @@ -380,7 +380,7 @@ private fun DebugView( Text(text = "Camera position is ${cameraPositionState.position}") Spacer(modifier = Modifier.height(4.dp)) val dragging = - if (markerState.dragState == DragState.DRAG) "dragging" else "not dragging" + if (markerState.isDragging) "dragging" else "not dragging" Text(text = "Marker is $dragging") Text(text = "Marker position is ${markerState.position}") } diff --git a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt index f7fadb44..2159e641 100644 --- a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt @@ -32,6 +32,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import com.google.maps.android.compose.marker.AdvancedMarkersActivity +import com.google.maps.android.compose.marker.MarkerClusteringActivity +import com.google.maps.android.compose.marker.markerdragevents.MarkerDragEventsActivity +import com.google.maps.android.compose.marker.markerscollection.MarkersCollectionActivity +import com.google.maps.android.compose.marker.syncingdraggablemarkerwithdatamodel.SyncingDraggableMarkerWithDataModelActivity +import com.google.maps.android.compose.marker.updatingnodragmarkerwithdatamodel.UpdatingNoDragMarkerWithDataModelActivity import com.google.maps.android.compose.theme.MapsComposeSampleTheme class MainActivity : ComponentActivity() { @@ -141,6 +147,34 @@ class MainActivity : ComponentActivity() { }) { Text(getString(R.string.recomposition_activity)) } + Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, MarkerDragEventsActivity::class.java)) + }) { + Text(getString(R.string.marker_drag_events_activity)) + } + Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, MarkersCollectionActivity::class.java)) + }) { + Text(getString(R.string.markers_collection_activity)) + } + Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, SyncingDraggableMarkerWithDataModelActivity::class.java)) + }) { + Text(getString(R.string.syncing_draggable_marker_with_data_model)) + } + Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, UpdatingNoDragMarkerWithDataModelActivity::class.java)) + }) { + Text(getString(R.string.updating_non_draggable_marker_with_data_model)) + } } } } diff --git a/app/src/main/java/com/google/maps/android/compose/AdvancedMarkersActivity.kt b/app/src/main/java/com/google/maps/android/compose/marker/AdvancedMarkersActivity.kt similarity index 94% rename from app/src/main/java/com/google/maps/android/compose/AdvancedMarkersActivity.kt rename to app/src/main/java/com/google/maps/android/compose/marker/AdvancedMarkersActivity.kt index 48cf92e8..f15e4e02 100644 --- a/app/src/main/java/com/google/maps/android/compose/AdvancedMarkersActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/marker/AdvancedMarkersActivity.kt @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.maps.android.compose +package com.google.maps.android.compose.marker import android.R.drawable.ic_menu_myplaces @@ -36,6 +36,12 @@ import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.PinConfig +import com.google.maps.android.compose.AdvancedMarker +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapProperties +import com.google.maps.android.compose.MapType +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.rememberMarkerState private const val TAG = "AdvancedMarkersActivity" diff --git a/app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt b/app/src/main/java/com/google/maps/android/compose/marker/MarkerClusteringActivity.kt similarity index 95% rename from app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt rename to app/src/main/java/com/google/maps/android/compose/marker/MarkerClusteringActivity.kt index 3f025e62..07ee1edb 100644 --- a/app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/marker/MarkerClusteringActivity.kt @@ -1,4 +1,4 @@ -package com.google.maps.android.compose +package com.google.maps.android.compose.marker import android.os.Bundle import android.util.Log @@ -40,9 +40,16 @@ import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import com.google.maps.android.clustering.ClusterItem import com.google.maps.android.clustering.algo.NonHierarchicalViewBasedAlgorithm +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MapsComposeExperimentalApi +import com.google.maps.android.compose.MarkerInfoWindow import com.google.maps.android.compose.clustering.Clustering import com.google.maps.android.compose.clustering.rememberClusterManager import com.google.maps.android.compose.clustering.rememberClusterRenderer +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.rememberMarkerState +import com.google.maps.android.compose.singapore +import com.google.maps.android.compose.singapore2 import kotlin.random.Random private val TAG = MarkerClusteringActivity::class.simpleName diff --git a/app/src/main/java/com/google/maps/android/compose/marker/markerdragevents/MarkerDragEventsActivity.kt b/app/src/main/java/com/google/maps/android/compose/marker/markerdragevents/MarkerDragEventsActivity.kt new file mode 100644 index 00000000..4c446e1c --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/marker/markerdragevents/MarkerDragEventsActivity.kt @@ -0,0 +1,127 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.maps.android.compose.marker.markerdragevents + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.defaultCameraPosition +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.rememberMarkerState +import com.google.maps.android.compose.singapore +import com.google.maps.android.compose.theme.MapsComposeSampleTheme +import kotlinx.coroutines.flow.dropWhile + +private val TAG = MarkerDragEventsActivity::class.simpleName + +/** + * Demonstrates how to reliably generate a sequence of Marker drag START-DRAG-END events as in the + * original GoogleMap Marker listener. + */ +class MarkerDragEventsActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MapsComposeSampleTheme { + GoogleMapWithMarker( + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +private fun GoogleMapWithMarker( + modifier: Modifier = Modifier, +) { + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState, + ) { + DraggableMarker( + onDragStart = { Log.i(TAG, "onDragStart") }, + onDragEnd = { Log.i(TAG, "onDragEnd") }, + onDrag = { position -> Log.i(TAG, "onDrag: $position") } + ) + } +} + +/** + * A draggable GoogleMap Marker. + * + * @param onDragStart called when marker dragging starts + * @param onDrag called with an update for the marker's current position during dragging + * @param onDragEnd called when marker dragging ends + */ +@Composable +private fun DraggableMarker( + onDragStart: () -> Unit = {}, + onDrag: (LatLng) -> Unit = {}, + onDragEnd: () -> Unit = {} +) { + val markerState = rememberMarkerState(position = singapore) + + Marker( + state = markerState, + draggable = true + ) + + LaunchedEffect(Unit) { + var inDrag = false + var priorPosition: LatLng? = singapore + + snapshotFlow { markerState.isDragging to markerState.position } + .dropWhile { (isDragging, position) -> + !isDragging && position == priorPosition // ignore initial value + } + .collect { (isDragging, position) -> + // Do not even bother to check isDragging state here: + // it is possible to miss a sequence of states + // where isDragging == true, then isDragging == false; + // in this case we would only see a change in position. + // (Hypothetically we could even miss a change in position + // if the Marker ended up in its original position at the + // end of the drag. But then nothing changed at all, + // so we should be ok to ignore this case altogether.) + if (!inDrag) { + inDrag = true + onDragStart() + } + + if (position != priorPosition) { + onDrag(position) + priorPosition = position + } + + if (!isDragging) { + inDrag = false + onDragEnd() + } + } + } +} diff --git a/app/src/main/java/com/google/maps/android/compose/marker/markerscollection/MarkersCollectionActivity.kt b/app/src/main/java/com/google/maps/android/compose/marker/markerscollection/MarkersCollectionActivity.kt new file mode 100644 index 00000000..23afa7b0 --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/marker/markerscollection/MarkersCollectionActivity.kt @@ -0,0 +1,154 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.maps.android.compose.marker.markerscollection + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.ui.Modifier +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.defaultCameraPosition +import com.google.maps.android.compose.marker.updatingnodragmarkerwithdatamodel.Marker +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.theme.MapsComposeSampleTheme + +/** + * Simplistic app data model intended for persistent storage. + * + * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data. + */ +private class DataModel { + /** + * Location data. + */ + val locationDataMap = mutableStateMapOf() +} + +/** + * Data type representing a location. + * + * This only stores location position, for demonstration purposes, + * but could hold other data related to the location. + */ +@Immutable +private data class LocationData(val position: LatLng) + +/** + * Unique, stable key for location + */ +private class LocationKey + +private typealias KeyedLocationData = Pair + +/** + * Demonstrates how to sync a data model with a changing collection of + * location markers using keys. + * + * The user can add a location marker to the model by clicking the map and delete a location from + * the model by clicking a marker. + * + * This example reuses the simple non-draggable Marker approach from the + * `UpdatingNoDragMarkerWithDataModelActivity` example, which encapsulates + * [MarkerState] to provide a cleaner API surface. + */ +class MarkersCollectionActivity : ComponentActivity() { + private val dataModel = DataModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MapsComposeSampleTheme { + Screen( + dataModel = dataModel, + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +private fun Screen( + dataModel: DataModel, + modifier: Modifier = Modifier +) = GoogleMapWithLocations( + modifier = modifier, + keyedLocationData = dataModel.locationDataMap.toList(), + onAddLocation = { locationData -> + dataModel.locationDataMap += LocationKey() to locationData + }, + onDeleteLocation = { key -> + dataModel.locationDataMap -= key + } +) + +/** + * A GoogleMap with locations represented by markers + * + * @param keyedLocationData model data for location markers. + * Uses a [Collection] type to keep it independent of our data model. + * @param onAddLocation location addition events for updating data model + * @param onDeleteLocation location deletion events for updating data model + */ +@Composable +private fun GoogleMapWithLocations( + keyedLocationData: Collection, + modifier: Modifier = Modifier, + onAddLocation: (LocationData) -> Unit, + onDeleteLocation: (LocationKey) -> Unit +) { + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState, + onMapClick = { position -> onAddLocation(LocationData(position)) } + ) { + Locations( + keyedLocationData = keyedLocationData, + onLocationClick = onDeleteLocation + ) + } +} + +/** + * Renders locations on a GoogleMap + * + * @param keyedLocationData model data for location markers. + * @param onLocationClick location click events + */ +@Composable +private fun Locations( + keyedLocationData: Collection, + onLocationClick: (LocationKey) -> Unit +) = keyedLocationData.forEach { (key, locationData) -> + key(key) { + Marker( + position = locationData.position, + onClick = { + onLocationClick(key) + true // consume click event to prevent camera move to marker + } + ) + } +} diff --git a/app/src/main/java/com/google/maps/android/compose/marker/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt b/app/src/main/java/com/google/maps/android/compose/marker/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt new file mode 100644 index 00000000..52ce1949 --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/marker/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt @@ -0,0 +1,168 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.maps.android.compose.marker.syncingdraggablemarkerwithdatamodel + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.defaultCameraPosition +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.singapore +import com.google.maps.android.compose.theme.MapsComposeSampleTheme + +/** + * Simplistic app data model intended for persistent storage. + * + * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data. + */ +private class DataModel { + /** + * Location data + */ + var locationData by mutableStateOf(LocationData(singapore)) +} + +/** + * Data type representing a location. + * + * This only stores location position, for demonstration purposes, + * but could hold other data related to the location. + */ +@Immutable +private data class LocationData(val position: LatLng) + +/** + * Demonstrates how to avoid data races when keeping a data model in sync + * with location derived from a draggable marker. The model is the initial source of truth for the + * marker's position; markers are draggable, so MarkerState becomes the source of truth after + * initialization. + * + * This addresses difficulties caused by having source of truth for position baked into + * com.google.android.gms.maps.model.Marker, and consequently MarkerState. + */ +class SyncingDraggableMarkerWithDataModelActivity : ComponentActivity() { + private val dataModel = DataModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MapsComposeSampleTheme { + Screen( + dataModel = dataModel, + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +private fun Screen( + dataModel: DataModel, + modifier: Modifier = Modifier +) { + GoogleMapWithLocation( + modifier = modifier, + locationData = dataModel.locationData, + onUpdateLocation = { locationData -> + dataModel.locationData = locationData + } + ) +} + +/** + * A GoogleMap with a location represented by a marker + * + * @param locationData model data for location marker. The UI becomes the source of truth for + * marker position after initial composition; the model's position is ignored on recomposition. + * @param onUpdateLocation location update events for updating data model + */ +@Composable +private fun GoogleMapWithLocation( + locationData: LocationData, + modifier: Modifier = Modifier, + onUpdateLocation: (LocationData) -> Unit +) { + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState, + ) { + LocationMarker( + locationData = locationData, + onLocationUpdate = onUpdateLocation + ) + } +} + +/** + * A draggable GoogleMap Marker representing a location on the map. + * + * @param locationData model data for location marker. The UI becomes the source of truth for + * marker position after initial composition; the model's position is ignored on recomposition. + * @param onLocationUpdate marker update events with updated [LocationData] + */ +@Composable +private fun LocationMarker( + locationData: LocationData, + onLocationUpdate: (LocationData) -> Unit +) { + // This sets the MarkerData from our model once (model is initial source of truth) + // and never updates it from the model afterwards, + // because MarkerState/GoogleMap is the source of truth after initialization + // and we want to avoid multiple competing sources of truth + // to prevent potential data races. + // This achieves a clean separation of sources of truth at the cost of + // no longer having state flow down. + // It is the price we pay for having source of truth baked into + // com.google.android.gms.maps.model.Marker, and consequently MarkerState. + // + // Do not use rememberMarkerState() here, because it uses rememberSaveable(); + // we want to save the position to persistent storage as part of our data model + // instead - rememberSaveable() would add a conflicting source of truth. + val markerState = remember { MarkerState(locationData.position) } + + Marker( + state = markerState, + draggable = true + ) + + LaunchedEffect(Unit) { + snapshotFlow { markerState.position } + .collect { position -> + // build LocationData update from marker update + val update = LocationData(position = position) + + // send update event + onLocationUpdate(update) + } + } +} diff --git a/app/src/main/java/com/google/maps/android/compose/marker/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt b/app/src/main/java/com/google/maps/android/compose/marker/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt new file mode 100644 index 00000000..2950d980 --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/marker/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt @@ -0,0 +1,154 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.maps.android.compose.marker.updatingnodragmarkerwithdatamodel + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.defaultCameraPosition +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.rememberMarkerState +import com.google.maps.android.compose.singapore +import com.google.maps.android.compose.singapore2 +import com.google.maps.android.compose.singapore3 +import com.google.maps.android.compose.theme.MapsComposeSampleTheme +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.random.Random + +/** + * Simplistic app data model intended for persistent storage. + * + * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data. + */ +private class DataModel { + /** + * Location data + */ + var locationData by mutableStateOf(LocationData(singapore)) +} + +/** + * Data type representing a location. + * + * This only stores location position, for demonstration purposes, + * but could hold other data related to the location. + */ +@Immutable +private data class LocationData(val position: LatLng) + +/** + * Demonstrates how to easily initialize and update position for a non-draggable + * Marker from a data model. + */ +class UpdatingNoDragMarkerWithDataModelActivity : ComponentActivity() { + private val dataModel = DataModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + lifecycleScope.launch { + // Simulate remote updates to data model + while (true) { + delay(3_000) + + val newPosition = when (Random.nextInt(3)) { + 0 -> singapore + 1 -> singapore2 + 2 -> singapore3 + else -> singapore + } + + dataModel.locationData = LocationData(newPosition) + } + } + + setContent { + MapsComposeSampleTheme { + GoogleMapWithSimpleMarker( + locationData = dataModel.locationData, + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +private fun GoogleMapWithSimpleMarker( + locationData: LocationData, + modifier: Modifier = Modifier, +) { + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState, + ) { + Marker(position = locationData.position) + } +} + +/** + * Standard API pattern for a non-draggable Marker. + * + * The caller does not have to deal with MarkerState, + * and can update Marker [position] via recomposition. + */ +@Composable +fun Marker( + position: LatLng, + onClick: () -> Boolean = { false }, +) { + val markerState = rememberUpdatedMarkerState(position) + + Marker( + state = markerState, + onClick = { onClick() } + ) +} + +/** + * Standard API pattern for remembering, initializing, and updating MarkerState for a + * non-draggable Marker, where [position] comes from a data model. + * + * Implementation modeled after `rememberUpdatedState`. + * + * This one uses [remember] behind the scenes, not [rememberMarkerState], which uses + * `rememberSaveable`. Our data model is the source of truth - `rememberSaveable` would + * create a conflicting source of truth. + */ +@Composable +fun rememberUpdatedMarkerState(position: LatLng): MarkerState = + // This pattern is equivalent to what rememberUpdatedState() does: + // rememberUpdatedState() uses MutableState, we use MarkerState. + // This is more efficient than updating position in an effect, + // as we avoid an additional recomposition. + remember { MarkerState(position = position) }.also { + it.position = position + } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b74c4b9..fa8528ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,8 +21,12 @@ Advanced Markers Map In Column Marker Clustering + Marker Drag Events + Markers Collection Location Tracking Scale Bar + Syncing Draggable Marker With Model + Updating Non-Draggable Marker With Model Recomposition Map Street View Custom Location Button diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt index 36e488b0..f1793069 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt @@ -142,45 +142,70 @@ internal class MapApplier( ) } map.setOnMarkerDragListener(object : GoogleMap.OnMarkerDragListener { - override fun onMarkerDrag(marker: Marker) { + // We update MarkerState isDragging & position properties in a specific well-defined + // order: MarkerState.position is never updated by us unless + // MarkerState.isDragging == true. This avoids using Snapshots, which can fail to apply; + // they would not be meaningful here, because we are not the actual source of truth. + + override fun onMarkerDragStart(marker: Marker) { decorations.findInputCallback( nodeMatchPredicate = { it.marker == marker }, + marker = marker, nodeInputCallback = { { - markerState.position = it.position - markerState.dragState = DragState.DRAG + val position = it.position + + markerState.isDragging = true + // update position after enabling isDragging + markerState.position = position + + @Suppress("DEPRECATION") + markerState.dragState = DragState.START } }, - marker = marker, - inputHandlerCallback = { onMarkerDrag } + inputHandlerCallback = { onMarkerDragStart } ) } - override fun onMarkerDragEnd(marker: Marker) { + override fun onMarkerDrag(marker: Marker) { decorations.findInputCallback( nodeMatchPredicate = { it.marker == marker }, - marker = marker, nodeInputCallback = { { - markerState.position = it.position - markerState.dragState = DragState.END + val position = it.position + + markerState.isDragging = true // just in case, should be set already + // update position after enabling isDragging + markerState.position = position + + @Suppress("DEPRECATION") + markerState.dragState = DragState.DRAG } }, - inputHandlerCallback = { onMarkerDragEnd } + marker = marker, + inputHandlerCallback = { onMarkerDrag } ) } - override fun onMarkerDragStart(marker: Marker) { + override fun onMarkerDragEnd(marker: Marker) { decorations.findInputCallback( nodeMatchPredicate = { it.marker == marker }, marker = marker, nodeInputCallback = { { - markerState.position = it.position - markerState.dragState = DragState.START + val position = it.position + + markerState.isDragging = true // just in case, should be set already + // update position after enabling isDragging + markerState.position = position + // disable isDragging after updating position + markerState.isDragging = false + + @Suppress("DEPRECATION") + markerState.dragState = DragState.END } }, - inputHandlerCallback = { onMarkerDragStart } + inputHandlerCallback = { onMarkerDragEnd } ) } }) diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt index 796ce3a3..25ddab7e 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt @@ -63,6 +63,7 @@ internal class MarkerNode( } @Immutable +@Deprecated("START, DRAG, END are events, not states. Avoid usage.") public enum class DragState { START, DRAG, END } @@ -70,6 +71,8 @@ public enum class DragState { /** * A state object that can be hoisted to control and observe the marker state. * + * This cannot be used to preserve marker info window visibility across configuration changes. + * * @param position the initial marker position */ public class MarkerState( @@ -77,12 +80,35 @@ public class MarkerState( ) { /** * Current position of the marker. + * + * This property is backed by Compose state. + * It can be updated by the API user and by the API itself: + * it has two potentially competing sources of truth. + * + * The API will not update the property unless [isDragging] `== true`, + * which will happen if and only if a Marker is draggable and the end user + * is currently dragging it. */ public var position: LatLng by mutableStateOf(position) + /** + * Reflects whether the end user is currently dragging the marker. + * Dragging can happen only if a Marker is draggable. + * + * This property is backed by Compose state. + */ + public var isDragging: Boolean by mutableStateOf(false) + internal set + /** * Current [DragState] of the marker. */ + @Deprecated( + "Use isDragging instead - dragState is not appropriate for representing \"state\";" + + " it is a lossy representation of drag \"events\", promoting invalid usage.", + level = DeprecationLevel.WARNING + ) + @Suppress("DEPRECATION") public var dragState: DragState by mutableStateOf(DragState.END) internal set @@ -99,14 +125,28 @@ public class MarkerState( } /** - * Shows the info window for the underlying marker + * Shows the info window for the underlying marker. + * + * Not backed by Compose state to accommodate + * [com.google.android.gms.maps.GoogleMap] special semantics: + * only a single info window can be visible for the entire GoogleMap. + * + * Only use from Compose Effect APIs, never directly from composition, to avoid exceptions and + * unexpected behavior from cancelled compositions. */ public fun showInfoWindow() { marker?.showInfoWindow() } /** - * Hides the info window for the underlying marker + * Hides the info window for the underlying marker. + * + * Not backed by observable Compose state to accommodate + * [com.google.android.gms.maps.GoogleMap] special semantics: + * only a single info window can be visible for the entire GoogleMap. + * + * Only use from Compose Effect APIs, never directly from composition, to avoid + * unexpected behavior from cancelled compositions. */ public fun hideInfoWindow() { marker?.hideInfoWindow() @@ -115,6 +155,9 @@ public class MarkerState( public companion object { /** * The default saver implementation for [MarkerState] + * + * This cannot be used to preserve marker info window visibility across + * configuration changes. */ public val Saver: Saver = Saver( save = { it.position }, @@ -123,6 +166,14 @@ public class MarkerState( } } +/** + * Uses [rememberSaveable] to retain [MarkerState.position] across configuration changes, + * for simple use cases. + * + * Other use cases may be better served syncing [MarkerState.position] with a data model. + * + * This cannot be used to preserve info window visibility across configuration changes. + */ @Composable public fun rememberMarkerState( key: String? = null, From 0d3ae1f187d3877de36b9a1e8dda1ac4315074a5 Mon Sep 17 00:00:00 2001 From: Uli Bubenheimer Date: Tue, 30 Jan 2024 12:12:17 -0500 Subject: [PATCH 2/6] Add an example for how to efficiently create a derived list of MarkerStates from a changing model list of marker positions by collecting results from key() composable Rename marker examples folder to markerexamples for clarity --- app/src/main/AndroidManifest.xml | 15 +- .../maps/android/compose/MainActivity.kt | 20 +- .../AdvancedMarkersActivity.kt | 2 +- .../MarkerClusteringActivity.kt | 2 +- ...bleMarkersCollectionWithPolygonActivity.kt | 212 ++++++++++++++++++ .../MarkerDragEventsActivity.kt | 2 +- .../MarkersCollectionActivity.kt | 4 +- ...ingDraggableMarkerWithDataModelActivity.kt | 2 +- ...datingNoDragMarkerWithDataModelActivity.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 10 files changed, 243 insertions(+), 19 deletions(-) rename app/src/main/java/com/google/maps/android/compose/{marker => markerexamples}/AdvancedMarkersActivity.kt (99%) rename app/src/main/java/com/google/maps/android/compose/{marker => markerexamples}/MarkerClusteringActivity.kt (99%) create mode 100644 app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt rename app/src/main/java/com/google/maps/android/compose/{marker => markerexamples}/markerdragevents/MarkerDragEventsActivity.kt (98%) rename app/src/main/java/com/google/maps/android/compose/{marker => markerexamples}/markerscollection/MarkersCollectionActivity.kt (96%) rename app/src/main/java/com/google/maps/android/compose/{marker => markerexamples}/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt (98%) rename app/src/main/java/com/google/maps/android/compose/{marker => markerexamples}/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt (98%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89a741d4..c3b5aa63 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,13 +43,13 @@ android:name=".BasicMapActivity" android:exported="false" /> + diff --git a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt index 2159e641..42ef0c0e 100644 --- a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt @@ -32,12 +32,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import com.google.maps.android.compose.marker.AdvancedMarkersActivity -import com.google.maps.android.compose.marker.MarkerClusteringActivity -import com.google.maps.android.compose.marker.markerdragevents.MarkerDragEventsActivity -import com.google.maps.android.compose.marker.markerscollection.MarkersCollectionActivity -import com.google.maps.android.compose.marker.syncingdraggablemarkerwithdatamodel.SyncingDraggableMarkerWithDataModelActivity -import com.google.maps.android.compose.marker.updatingnodragmarkerwithdatamodel.UpdatingNoDragMarkerWithDataModelActivity +import com.google.maps.android.compose.markerexamples.AdvancedMarkersActivity +import com.google.maps.android.compose.markerexamples.MarkerClusteringActivity +import com.google.maps.android.compose.markerexamples.draggablemarkerscollectionwithpolygon.DraggableMarkersCollectionWithPolygonActivity +import com.google.maps.android.compose.markerexamples.markerdragevents.MarkerDragEventsActivity +import com.google.maps.android.compose.markerexamples.markerscollection.MarkersCollectionActivity +import com.google.maps.android.compose.markerexamples.syncingdraggablemarkerwithdatamodel.SyncingDraggableMarkerWithDataModelActivity +import com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel.UpdatingNoDragMarkerWithDataModelActivity import com.google.maps.android.compose.theme.MapsComposeSampleTheme class MainActivity : ComponentActivity() { @@ -175,6 +176,13 @@ class MainActivity : ComponentActivity() { }) { Text(getString(R.string.updating_non_draggable_marker_with_data_model)) } + Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, DraggableMarkersCollectionWithPolygonActivity::class.java)) + }) { + Text(getString(R.string.draggable_markers_collection_with_polygon)) + } } } } diff --git a/app/src/main/java/com/google/maps/android/compose/marker/AdvancedMarkersActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt similarity index 99% rename from app/src/main/java/com/google/maps/android/compose/marker/AdvancedMarkersActivity.kt rename to app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt index f15e4e02..e32b08a1 100644 --- a/app/src/main/java/com/google/maps/android/compose/marker/AdvancedMarkersActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/AdvancedMarkersActivity.kt @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.maps.android.compose.marker +package com.google.maps.android.compose.markerexamples import android.R.drawable.ic_menu_myplaces diff --git a/app/src/main/java/com/google/maps/android/compose/marker/MarkerClusteringActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt similarity index 99% rename from app/src/main/java/com/google/maps/android/compose/marker/MarkerClusteringActivity.kt rename to app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt index 07ee1edb..8301b006 100644 --- a/app/src/main/java/com/google/maps/android/compose/marker/MarkerClusteringActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt @@ -1,4 +1,4 @@ -package com.google.maps.android.compose.marker +package com.google.maps.android.compose.markerexamples import android.os.Bundle import android.util.Log diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt new file mode 100644 index 00000000..046aff0e --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt @@ -0,0 +1,212 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.maps.android.compose.markerexamples.draggablemarkerscollectionwithpolygon + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.GoogleMap +import com.google.maps.android.compose.Marker +import com.google.maps.android.compose.MarkerState +import com.google.maps.android.compose.Polygon +import com.google.maps.android.compose.defaultCameraPosition +import com.google.maps.android.compose.rememberCameraPositionState +import com.google.maps.android.compose.theme.MapsComposeSampleTheme +import kotlinx.coroutines.flow.drop + +/** + * Simplistic app data model intended for persistent storage. + * + * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data. + */ +private class DataModel { + /** + * Location data. + */ + val locationDataMap = mutableStateMapOf() +} + +/** + * Data type representing a location. + * + * This only stores location position, for demonstration purposes, + * but could hold other data related to the location. + */ +@Immutable +private data class LocationData(val position: LatLng) + +/** + * Unique, stable key for location + */ +private class LocationKey + +private typealias KeyedLocationData = Pair + +/** + * Demonstrates how to sync a data model with a changing collection of + * draggable markers using keys, while keeping a Polygon of the marker positions in sync with + * the current marker position. When dragging: the data model is updated only once dragging has + * ended (user released marker), as a model update might trigger other costly operations. + * + * The user can add a location marker to the model by clicking the map and delete a location from + * the model by clicking a marker. + * + * This example builds on top of ideas from MarkersCollectionActivity and + * SyncingDraggableMarkerWithDataModelActivity. + */ +class DraggableMarkersCollectionWithPolygonActivity : ComponentActivity() { + private val dataModel = DataModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MapsComposeSampleTheme { + Screen( + dataModel = dataModel, + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +private fun Screen( + dataModel: DataModel, + modifier: Modifier = Modifier +) = GoogleMapWithLocations( + modifier = modifier, + keyedLocationData = dataModel.locationDataMap.toList(), + onAddLocation = { locationData -> + dataModel.locationDataMap += LocationKey() to locationData + }, + onDeleteLocation = { key -> + dataModel.locationDataMap -= key + }, + onMoveLocation = { key, locationData -> + dataModel.locationDataMap[key] = locationData + } +) + +/** + * A GoogleMap with locations represented by markers + */ +@Composable +private fun GoogleMapWithLocations( + keyedLocationData: Collection, + modifier: Modifier = Modifier, + onAddLocation: (LocationData) -> Unit, + onDeleteLocation: (LocationKey) -> Unit, + onMoveLocation: (LocationKey, LocationData) -> Unit +) { + val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } + + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState, + onMapClick = { position -> onAddLocation(LocationData(position)) } + ) { + Locations( + keyedLocationData = keyedLocationData, + onLocationClick = onDeleteLocation, + onLocationUpdate = onMoveLocation + ) + } +} + +/** + * Renders locations on a GoogleMap + */ +@Composable +private fun Locations( + keyedLocationData: Collection, + onLocationClick: (LocationKey) -> Unit, + onLocationUpdate: (LocationKey, LocationData) -> Unit +) { + // This doubles as a handy trick to leverage Compose's node matching algorithm for + // generating a list of MarkerStates derived from the original model + val markerStates = keyedLocationData.map { (key, locationData) -> + key(key) { + // This sets the MarkerData from our model once (model is initial source of truth) + // and never updates it from the model afterwards. + // See SyncingDraggableMarkerWithDataModelActivity for rationale. + val markerState = remember { MarkerState(locationData.position) } + + LocationMarker( + markerState, + onClick = { onLocationClick(key) }, + onDragEnd = { + val newLocationData = LocationData(markerState.position) + onLocationUpdate(key, newLocationData) + } + ) + + markerState + } + } + + Polygon(markerStates) +} + +/** + * A draggable GoogleMap Marker representing a location on the map + */ +@Composable +private fun LocationMarker( + markerState: MarkerState, + onClick: () -> Unit, + onDragEnd: () -> Unit +) { + Marker( + state = markerState, + draggable = true, + onClick = { + onClick() + + true + } + ) + + LaunchedEffect(Unit) { + snapshotFlow { markerState.isDragging } + .drop(1) // ignore initial value + .collect { isDragging -> + if (!isDragging) onDragEnd() + } + } +} + +/** + * A Polygon. Helps isolate recompositions while a Marker is being dragged. + */ +@Composable +private fun Polygon(markerStates: List) { + if (markerStates.isNotEmpty()) { + val markerPositions = markerStates.map { it.position } + + Polygon(markerPositions) + } +} diff --git a/app/src/main/java/com/google/maps/android/compose/marker/markerdragevents/MarkerDragEventsActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt similarity index 98% rename from app/src/main/java/com/google/maps/android/compose/marker/markerdragevents/MarkerDragEventsActivity.kt rename to app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt index 4c446e1c..5862ce60 100644 --- a/app/src/main/java/com/google/maps/android/compose/marker/markerdragevents/MarkerDragEventsActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerdragevents/MarkerDragEventsActivity.kt @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.maps.android.compose.marker.markerdragevents +package com.google.maps.android.compose.markerexamples.markerdragevents import android.os.Bundle import android.util.Log diff --git a/app/src/main/java/com/google/maps/android/compose/marker/markerscollection/MarkersCollectionActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt similarity index 96% rename from app/src/main/java/com/google/maps/android/compose/marker/markerscollection/MarkersCollectionActivity.kt rename to app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt index 23afa7b0..f07f37c8 100644 --- a/app/src/main/java/com/google/maps/android/compose/marker/markerscollection/MarkersCollectionActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.maps.android.compose.marker.markerscollection +package com.google.maps.android.compose.markerexamples.markerscollection import android.os.Bundle import androidx.activity.ComponentActivity @@ -27,7 +27,7 @@ import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.GoogleMap import com.google.maps.android.compose.MarkerState import com.google.maps.android.compose.defaultCameraPosition -import com.google.maps.android.compose.marker.updatingnodragmarkerwithdatamodel.Marker +import com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel.Marker import com.google.maps.android.compose.rememberCameraPositionState import com.google.maps.android.compose.theme.MapsComposeSampleTheme diff --git a/app/src/main/java/com/google/maps/android/compose/marker/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt similarity index 98% rename from app/src/main/java/com/google/maps/android/compose/marker/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt rename to app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt index 52ce1949..1aa48ae9 100644 --- a/app/src/main/java/com/google/maps/android/compose/marker/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/syncingdraggablemarkerwithdatamodel/SyncingDraggableMarkerWithDataModelActivity.kt @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.maps.android.compose.marker.syncingdraggablemarkerwithdatamodel +package com.google.maps.android.compose.markerexamples.syncingdraggablemarkerwithdatamodel import android.os.Bundle import androidx.activity.ComponentActivity diff --git a/app/src/main/java/com/google/maps/android/compose/marker/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt similarity index 98% rename from app/src/main/java/com/google/maps/android/compose/marker/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt rename to app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt index 2950d980..ecd58208 100644 --- a/app/src/main/java/com/google/maps/android/compose/marker/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/updatingnodragmarkerwithdatamodel/UpdatingNoDragMarkerWithDataModelActivity.kt @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.maps.android.compose.marker.updatingnodragmarkerwithdatamodel +package com.google.maps.android.compose.markerexamples.updatingnodragmarkerwithdatamodel import android.os.Bundle import androidx.activity.ComponentActivity diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa8528ab..6638f6ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ Scale Bar Syncing Draggable Marker With Model Updating Non-Draggable Marker With Model + Polygon around draggable markers Recomposition Map Street View Custom Location Button From 7e4d48c54439945165d38e930148280bedd0f635 Mon Sep 17 00:00:00 2001 From: Uli Bubenheimer Date: Wed, 31 Jan 2024 12:20:03 -0500 Subject: [PATCH 3/6] Refine example --- ...raggableMarkersCollectionWithPolygonActivity.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt index 046aff0e..1c3f62f3 100644 --- a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt @@ -147,8 +147,8 @@ private fun Locations( onLocationUpdate: (LocationKey, LocationData) -> Unit ) { // This doubles as a handy trick to leverage Compose's node matching algorithm for - // generating a list of MarkerStates derived from the original model - val markerStates = keyedLocationData.map { (key, locationData) -> + // generating a list of marker positions derived from the original model + val movingVertices: List<() -> LatLng> = keyedLocationData.map { (key, locationData) -> key(key) { // This sets the MarkerData from our model once (model is initial source of truth) // and never updates it from the model afterwards. @@ -164,11 +164,11 @@ private fun Locations( } ) - markerState + markerState::position // share only read access to MarkerState.position } } - Polygon(markerStates) + Polygon(movingVertices) } /** @@ -203,9 +203,9 @@ private fun LocationMarker( * A Polygon. Helps isolate recompositions while a Marker is being dragged. */ @Composable -private fun Polygon(markerStates: List) { - if (markerStates.isNotEmpty()) { - val markerPositions = markerStates.map { it.position } +private fun Polygon(movingVertices: List<() -> LatLng>) { + if (movingVertices.isNotEmpty()) { + val markerPositions = movingVertices.map { it() } Polygon(markerPositions) } From 04342cf50648aadfa5fc6f1b5ef64b9888763658 Mon Sep 17 00:00:00 2001 From: Uli Bubenheimer Date: Fri, 24 May 2024 09:08:03 -0400 Subject: [PATCH 4/6] Improve KDoc for example --- .../markerscollection/MarkersCollectionActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt index f07f37c8..dacb43ac 100644 --- a/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/markerscollection/MarkersCollectionActivity.kt @@ -105,7 +105,7 @@ private fun Screen( /** * A GoogleMap with locations represented by markers * - * @param keyedLocationData model data for location markers. + * @param keyedLocationData model data for location markers with unique keys. * Uses a [Collection] type to keep it independent of our data model. * @param onAddLocation location addition events for updating data model * @param onDeleteLocation location deletion events for updating data model @@ -134,7 +134,7 @@ private fun GoogleMapWithLocations( /** * Renders locations on a GoogleMap * - * @param keyedLocationData model data for location markers. + * @param keyedLocationData model data for location markers with unique keys. * @param onLocationClick location click events */ @Composable From 765c6fe1860c72822adc3d822baed0bc2e6d533b Mon Sep 17 00:00:00 2001 From: Uli Bubenheimer Date: Tue, 28 May 2024 22:31:51 -0400 Subject: [PATCH 5/6] Improve KDoc for example --- .../DraggableMarkersCollectionWithPolygonActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt index 1c3f62f3..217967e2 100644 --- a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt @@ -146,7 +146,7 @@ private fun Locations( onLocationClick: (LocationKey) -> Unit, onLocationUpdate: (LocationKey, LocationData) -> Unit ) { - // This doubles as a handy trick to leverage Compose's node matching algorithm for + // This doubles as a handy trick to leverage Compose's group matching algorithm for // generating a list of marker positions derived from the original model val movingVertices: List<() -> LatLng> = keyedLocationData.map { (key, locationData) -> key(key) { From fb673f9c0c8dc5fa584d18d58fe09f1aae913e8f Mon Sep 17 00:00:00 2001 From: Uli Bubenheimer Date: Wed, 29 May 2024 14:29:46 -0400 Subject: [PATCH 6/6] Simplify and improve example --- ...bleMarkersCollectionWithPolygonActivity.kt | 176 ++++++++---------- 1 file changed, 78 insertions(+), 98 deletions(-) diff --git a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt index 217967e2..8bea519c 100644 --- a/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/markerexamples/draggablemarkerscollectionwithpolygon/DraggableMarkersCollectionWithPolygonActivity.kt @@ -20,11 +20,13 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.ui.Modifier import com.google.android.gms.maps.model.LatLng import com.google.maps.android.compose.GoogleMap @@ -34,24 +36,11 @@ import com.google.maps.android.compose.Polygon import com.google.maps.android.compose.defaultCameraPosition import com.google.maps.android.compose.rememberCameraPositionState import com.google.maps.android.compose.theme.MapsComposeSampleTheme -import kotlinx.coroutines.flow.drop - -/** - * Simplistic app data model intended for persistent storage. - * - * This only stores [LocationData], for demonstration purposes, but could hold an entire app's data. - */ -private class DataModel { - /** - * Location data. - */ - val locationDataMap = mutableStateMapOf() -} /** * Data type representing a location. * - * This only stores location position, for demonstration purposes, + * This only stores location position, for illustration, * but could hold other data related to the location. */ @Immutable @@ -62,13 +51,60 @@ private data class LocationData(val position: LatLng) */ private class LocationKey -private typealias KeyedLocationData = Pair +/** + * Encapsulates mapping from data model to MarkerStates. Part of view model. + * MarkerStates are relegated to an implementation detail. + * Use new [DraggableMarkersModel] instance if data model is updated externally: + * MarkerStates are source of truth after initialization from data model. + */ +@Stable +private class DraggableMarkersModel(dataModel: Map) { + // This initializes MarkerState from our model once (model is initial source of truth) + // and never updates it from the model afterwards. + // See SyncingDraggableMarkerWithDataModelActivity for rationale. + private val markerDataMap: SnapshotStateMap = mutableStateMapOf( + *dataModel.entries.map { (locationKey, locationData) -> + locationKey to MarkerState(locationData.position) + }.toTypedArray() + ) + + /** Add new marker location to model */ + fun addLocation(locationData: LocationData) { + markerDataMap += LocationKey() to MarkerState(locationData.position) + } + + /** Delete marker location from model */ + private fun deleteLocation(locationKey: LocationKey) { + markerDataMap -= locationKey + } + + /** + * Render Markers from model + */ + @Composable + fun Markers() = markerDataMap.forEach { (locationKey, markerState) -> + key(locationKey) { + LocationMarker( + markerState, + onClick = { deleteLocation(locationKey) } + ) + } + } + + /** + * List of functions providing current positions of Markers. + * + * Calling from composition will trigger recomposition when Markers and their positions + * change. + */ + val markerPositionsModel: List<() -> LatLng> + get() = markerDataMap.values.map { { it.position } } +} /** * Demonstrates how to sync a data model with a changing collection of * draggable markers using keys, while keeping a Polygon of the marker positions in sync with - * the current marker position. When dragging: the data model is updated only once dragging has - * ended (user released marker), as a model update might trigger other costly operations. + * the current marker position. * * The user can add a location marker to the model by clicking the map and delete a location from * the model by clicking a marker. @@ -77,15 +113,23 @@ private typealias KeyedLocationData = Pair * SyncingDraggableMarkerWithDataModelActivity. */ class DraggableMarkersCollectionWithPolygonActivity : ComponentActivity() { - private val dataModel = DataModel() + // Simplistic data model from repository being set from outside (should be part of view model); + // Only stores [LocationData], for illustration, but could hold additional data. + private var dataModel: Map = mapOf() + set(value) { + field = value + markersModel = DraggableMarkersModel(value) + } + + private var markersModel by mutableStateOf(DraggableMarkersModel(dataModel)) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MapsComposeSampleTheme { - Screen( - dataModel = dataModel, + GoogleMapWithLocations( + markersModel, modifier = Modifier.fillMaxSize() ) } @@ -93,82 +137,25 @@ class DraggableMarkersCollectionWithPolygonActivity : ComponentActivity() { } } -@Composable -private fun Screen( - dataModel: DataModel, - modifier: Modifier = Modifier -) = GoogleMapWithLocations( - modifier = modifier, - keyedLocationData = dataModel.locationDataMap.toList(), - onAddLocation = { locationData -> - dataModel.locationDataMap += LocationKey() to locationData - }, - onDeleteLocation = { key -> - dataModel.locationDataMap -= key - }, - onMoveLocation = { key, locationData -> - dataModel.locationDataMap[key] = locationData - } -) - /** * A GoogleMap with locations represented by markers */ @Composable private fun GoogleMapWithLocations( - keyedLocationData: Collection, - modifier: Modifier = Modifier, - onAddLocation: (LocationData) -> Unit, - onDeleteLocation: (LocationKey) -> Unit, - onMoveLocation: (LocationKey, LocationData) -> Unit + markersModel: DraggableMarkersModel, + modifier: Modifier = Modifier ) { val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition } GoogleMap( modifier = modifier, cameraPositionState = cameraPositionState, - onMapClick = { position -> onAddLocation(LocationData(position)) } + onMapClick = { position -> markersModel.addLocation(LocationData(position)) } ) { - Locations( - keyedLocationData = keyedLocationData, - onLocationClick = onDeleteLocation, - onLocationUpdate = onMoveLocation - ) - } -} + markersModel.Markers() -/** - * Renders locations on a GoogleMap - */ -@Composable -private fun Locations( - keyedLocationData: Collection, - onLocationClick: (LocationKey) -> Unit, - onLocationUpdate: (LocationKey, LocationData) -> Unit -) { - // This doubles as a handy trick to leverage Compose's group matching algorithm for - // generating a list of marker positions derived from the original model - val movingVertices: List<() -> LatLng> = keyedLocationData.map { (key, locationData) -> - key(key) { - // This sets the MarkerData from our model once (model is initial source of truth) - // and never updates it from the model afterwards. - // See SyncingDraggableMarkerWithDataModelActivity for rationale. - val markerState = remember { MarkerState(locationData.position) } - - LocationMarker( - markerState, - onClick = { onLocationClick(key) }, - onDragEnd = { - val newLocationData = LocationData(markerState.position) - onLocationUpdate(key, newLocationData) - } - ) - - markerState::position // share only read access to MarkerState.position - } + Polygon(markersModel::markerPositionsModel) } - - Polygon(movingVertices) } /** @@ -177,8 +164,7 @@ private fun Locations( @Composable private fun LocationMarker( markerState: MarkerState, - onClick: () -> Unit, - onDragEnd: () -> Unit + onClick: () -> Unit ) { Marker( state = markerState, @@ -189,23 +175,17 @@ private fun LocationMarker( true } ) - - LaunchedEffect(Unit) { - snapshotFlow { markerState.isDragging } - .drop(1) // ignore initial value - .collect { isDragging -> - if (!isDragging) onDragEnd() - } - } } /** * A Polygon. Helps isolate recompositions while a Marker is being dragged. */ @Composable -private fun Polygon(movingVertices: List<() -> LatLng>) { - if (movingVertices.isNotEmpty()) { - val markerPositions = movingVertices.map { it() } +private fun Polygon(markerPositionsModel: () -> List<() -> LatLng>) { + val movingMarkerPositions = markerPositionsModel() + + if (movingMarkerPositions.isNotEmpty()) { + val markerPositions = movingMarkerPositions.map { it() } Polygon(markerPositions) }