diff --git a/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt index a3075fea..e1086324 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt @@ -28,8 +28,10 @@ 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.Spacer 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.layout.wrapContentSize import androidx.compose.material.Button @@ -49,7 +51,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import com.google.android.gms.maps.CameraUpdateFactory -import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng @@ -60,6 +61,7 @@ private const val TAG = "MapSampleActivity" val singapore = LatLng(1.35, 103.87) val singapore2 = LatLng(1.40, 103.77) +val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f) class MapSampleActivity : ComponentActivity() { @@ -69,7 +71,7 @@ class MapSampleActivity : ComponentActivity() { var isMapLoaded by remember { mutableStateOf(false) } // Observing and controlling the camera's state can be done with a CameraPositionState val cameraPositionState = rememberCameraPositionState { - position = CameraPosition.fromLatLngZoom(singapore, 11f) + position = defaultCameraPosition } Box(Modifier.fillMaxSize()) { @@ -106,6 +108,13 @@ fun GoogleMapView( cameraPositionState: CameraPositionState, onMapLoaded: () -> Unit, ) { + val singaporePositionState = rememberMarkerPositionState(position = singapore) + val singapore2PositionState = rememberMarkerPositionState(position = singapore2) + var circlePositionState by remember { mutableStateOf(singapore) } + if (singaporePositionState.dragState == DragState.END) { + circlePositionState = singaporePositionState.position + } + var uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) } var shouldAnimateZoom by remember { mutableStateOf(true) } var ticker by remember { mutableStateOf(0) } @@ -129,14 +138,15 @@ fun GoogleMapView( false } MarkerInfoWindowContent( - position = singapore, + positionState = singaporePositionState, title = "Zoom in has been tapped $ticker times.", onClick = markerClick, + draggable = true, ) { Text(it.title ?: "Title", color = Color.Red) } MarkerInfoWindowContent( - position = singapore2, + positionState = singapore2PositionState, title = "Marker with custom info window.\nZoom in has been tapped $ticker times.", icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE), onClick = markerClick, @@ -144,7 +154,7 @@ fun GoogleMapView( Text(it.title ?: "Title", color = Color.Blue) } Circle( - center = singapore, + center = circlePositionState, fillColor = MaterialTheme.colors.secondary, strokeColor = MaterialTheme.colors.secondaryVariant, radius = 1000.0, @@ -156,6 +166,20 @@ fun GoogleMapView( Log.d("GoogleMap", "Selected map type $it") mapProperties = mapProperties.copy(mapType = it) }) + Button( + modifier = Modifier.padding(4.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.onPrimary, + contentColor = MaterialTheme.colors.primary + ), + onClick = { + mapProperties = mapProperties.copy(mapType = MapType.NORMAL) + cameraPositionState.position = defaultCameraPosition + singaporePositionState.position = singapore + } + ) { + Text(text = "RESET MAP", style = MaterialTheme.typography.body1) + } val coroutineScope = rememberCoroutineScope() ZoomControls( shouldAnimateZoom, @@ -186,7 +210,7 @@ fun GoogleMapView( uiSettings = uiSettings.copy(zoomControlsEnabled = it) } ) - DebugView(cameraPositionState) + DebugView(cameraPositionState, singaporePositionState) } } @@ -263,7 +287,10 @@ private fun MapButton(text: String, onClick: () -> Unit) { } @Composable -private fun DebugView(cameraPositionState: CameraPositionState) { +private fun DebugView( + cameraPositionState: CameraPositionState, + markerPositionState: MarkerPositionState +) { Column( Modifier .fillMaxWidth(), @@ -273,5 +300,10 @@ private fun DebugView(cameraPositionState: CameraPositionState) { if (cameraPositionState.isMoving) "moving" else "not moving" Text(text = "Camera is $moving") Text(text = "Camera position is ${cameraPositionState.position}") + Spacer(modifier = Modifier.height(4.dp)) + val dragging = + if (markerPositionState.dragState == DragState.DRAG) "dragging" else "not dragging" + Text(text = "Marker is $dragging") + Text(text = "Marker position is ${markerPositionState.position}") } -} \ No newline at end of file +} 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 cacc5c49..14faaf6c 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 @@ -15,7 +15,6 @@ package com.google.maps.android.compose import androidx.compose.runtime.AbstractApplier -import androidx.compose.ui.platform.ComposeView import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.Circle @@ -112,21 +111,24 @@ internal class MapApplier( } map.setOnMarkerDragListener(object : GoogleMap.OnMarkerDragListener { override fun onMarkerDrag(marker: Marker) { - val markerDragState = - decorations.nodeForMarker(marker)?.markerDragState - markerDragState?.dragState = DragState.DRAG + with(decorations.nodeForMarker(marker)) { + this?.markerPositionState?.position = marker.position + this?.markerPositionState?.dragState = DragState.DRAG + } } override fun onMarkerDragEnd(marker: Marker) { - val markerDragState = - decorations.nodeForMarker(marker)?.markerDragState - markerDragState?.dragState = DragState.END + with(decorations.nodeForMarker(marker)) { + this?.markerPositionState?.position = marker.position + this?.markerPositionState?.dragState = DragState.END + } } override fun onMarkerDragStart(marker: Marker) { - val markerDragState = - decorations.nodeForMarker(marker)?.markerDragState - markerDragState?.dragState = DragState.START + with(decorations.nodeForMarker(marker)) { + this?.markerPositionState?.position = marker.position + this?.markerPositionState?.dragState = DragState.START + } } }) map.setInfoWindowAdapter( 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 722d22c9..6a70011a 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 @@ -21,8 +21,9 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.currentComposer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCompositionContext +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset import com.google.android.gms.maps.model.BitmapDescriptor @@ -33,7 +34,7 @@ import com.google.maps.android.ktx.addMarker internal class MarkerNode( val compositionContext: CompositionContext, val marker: Marker, - var markerDragState: MarkerDragState?, + val markerPositionState: MarkerPositionState, var onMarkerClick: (Marker) -> Boolean, var onInfoWindowClick: (Marker) -> Unit, var onInfoWindowClose: (Marker) -> Unit, @@ -52,28 +53,48 @@ enum class DragState { } /** - * A state object for observing marker drag events. + * A state object that can be hoisted to control and observe the marker state. + * + * @param position the initial marker position */ -class MarkerDragState { +class MarkerPositionState( + position: LatLng = LatLng(0.0, 0.0) +) { + /** + * Current position of the marker. + */ + var position: LatLng by mutableStateOf(position) + /** - * State of the marker drag. + * Current [DragState] of the marker. */ var dragState: DragState by mutableStateOf(DragState.END) internal set + + companion object { + /** + * The default saver implementation for [MarkerPositionState] + */ + val Saver = Saver( + save = { it.position }, + restore = { MarkerPositionState(it) } + ) + } } -/** - * Creates and [remember] a [MarkerDragState]. - */ @Composable -fun rememberMarkerDragState(): MarkerDragState = remember { - MarkerDragState() +fun rememberMarkerPositionState( + key: String? = null, + position: LatLng = LatLng(0.0, 0.0) +): MarkerPositionState = rememberSaveable(key = key, saver = MarkerPositionState.Saver) { + MarkerPositionState(position) } /** * A composable for a marker on the map. * - * @param position the position of the marker + * @param positionState the [MarkerPositionState] to be used to control or observe the marker + * position state * @param alpha the alpha (opacity) of the marker * @param anchor the anchor for the marker image * @param draggable sets the draggability for the marker @@ -86,7 +107,6 @@ fun rememberMarkerDragState(): MarkerDragState = remember { * @param title the title for the marker * @param visible the visibility of the marker * @param zIndex the z-index of the marker - * @param markerDragState a [MarkerDragState] to be used for observing marker drag events * @param onClick a lambda invoked when the marker is clicked * @param onInfoWindowClick a lambda invoked when the marker's info window is clicked * @param onInfoWindowClose a lambda invoked when the marker's info window is closed @@ -94,7 +114,7 @@ fun rememberMarkerDragState(): MarkerDragState = remember { */ @Composable fun Marker( - position: LatLng, + positionState: MarkerPositionState = rememberMarkerPositionState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, @@ -107,14 +127,13 @@ fun Marker( title: String? = null, visible: Boolean = true, zIndex: Float = 0.0f, - markerDragState: MarkerDragState? = null, onClick: (Marker) -> Boolean = { false }, onInfoWindowClick: (Marker) -> Unit = {}, onInfoWindowClose: (Marker) -> Unit = {}, onInfoWindowLongClick: (Marker) -> Unit = {}, ) { MarkerImpl( - position = position, + positionState = positionState, alpha = alpha, anchor = anchor, draggable = draggable, @@ -127,7 +146,6 @@ fun Marker( title = title, visible = visible, zIndex = zIndex, - markerDragState = markerDragState, onClick = onClick, onInfoWindowClick = onInfoWindowClick, onInfoWindowClose = onInfoWindowClose, @@ -140,7 +158,8 @@ fun Marker( * customized. If this customization is not required, use * [com.google.maps.android.compose.Marker]. * - * @param position the position of the marker + * @param positionState the [MarkerPositionState] to be used to control or observe the marker + * position state * @param alpha the alpha (opacity) of the marker * @param anchor the anchor for the marker image * @param draggable sets the draggability for the marker @@ -153,7 +172,6 @@ fun Marker( * @param title the title for the marker * @param visible the visibility of the marker * @param zIndex the z-index of the marker - * @param markerDragState a [MarkerDragState] to be used for observing marker drag events * @param onClick a lambda invoked when the marker is clicked * @param onInfoWindowClick a lambda invoked when the marker's info window is clicked * @param onInfoWindowClose a lambda invoked when the marker's info window is closed @@ -163,7 +181,7 @@ fun Marker( */ @Composable fun MarkerInfoWindow( - position: LatLng, + positionState: MarkerPositionState = rememberMarkerPositionState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, @@ -176,7 +194,6 @@ fun MarkerInfoWindow( title: String? = null, visible: Boolean = true, zIndex: Float = 0.0f, - markerDragState: MarkerDragState? = null, onClick: (Marker) -> Boolean = { false }, onInfoWindowClick: (Marker) -> Unit = {}, onInfoWindowClose: (Marker) -> Unit = {}, @@ -184,7 +201,7 @@ fun MarkerInfoWindow( content: (@Composable (Marker) -> Unit)? = null ) { MarkerImpl( - position = position, + positionState = positionState, alpha = alpha, anchor = anchor, draggable = draggable, @@ -197,7 +214,6 @@ fun MarkerInfoWindow( title = title, visible = visible, zIndex = zIndex, - markerDragState = markerDragState, onClick = onClick, onInfoWindowClick = onInfoWindowClick, onInfoWindowClose = onInfoWindowClose, @@ -211,7 +227,8 @@ fun MarkerInfoWindow( * customized. If this customization is not required, use * [com.google.maps.android.compose.Marker]. * - * @param position the position of the marker + * @param positionState the [MarkerPositionState] to be used to control or observe the marker + * position state * @param alpha the alpha (opacity) of the marker * @param anchor the anchor for the marker image * @param draggable sets the draggability for the marker @@ -224,7 +241,6 @@ fun MarkerInfoWindow( * @param title the title for the marker * @param visible the visibility of the marker * @param zIndex the z-index of the marker - * @param markerDragState a [MarkerDragState] to be used for observing marker drag events * @param onClick a lambda invoked when the marker is clicked * @param onInfoWindowClick a lambda invoked when the marker's info window is clicked * @param onInfoWindowClose a lambda invoked when the marker's info window is closed @@ -234,7 +250,7 @@ fun MarkerInfoWindow( */ @Composable fun MarkerInfoWindowContent( - position: LatLng, + positionState: MarkerPositionState = rememberMarkerPositionState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, @@ -247,7 +263,6 @@ fun MarkerInfoWindowContent( title: String? = null, visible: Boolean = true, zIndex: Float = 0.0f, - markerDragState: MarkerDragState? = null, onClick: (Marker) -> Boolean = { false }, onInfoWindowClick: (Marker) -> Unit = {}, onInfoWindowClose: (Marker) -> Unit = {}, @@ -255,7 +270,7 @@ fun MarkerInfoWindowContent( content: (@Composable (Marker) -> Unit)? = null ) { MarkerImpl( - position = position, + positionState = positionState, alpha = alpha, anchor = anchor, draggable = draggable, @@ -268,7 +283,6 @@ fun MarkerInfoWindowContent( title = title, visible = visible, zIndex = zIndex, - markerDragState = markerDragState, onClick = onClick, onInfoWindowClick = onInfoWindowClick, onInfoWindowClose = onInfoWindowClose, @@ -280,7 +294,8 @@ fun MarkerInfoWindowContent( /** * Internal implementation for a marker on a Google map. * - * @param position the position of the marker + * @param positionState the [MarkerPositionState] to be used to control or observe the marker + * position state * @param alpha the alpha (opacity) of the marker * @param anchor the anchor for the marker image * @param draggable sets the draggability for the marker @@ -293,7 +308,6 @@ fun MarkerInfoWindowContent( * @param title the title for the marker * @param visible the visibility of the marker * @param zIndex the z-index of the marker - * @param markerDragState a [MarkerDragState] to be used for observing marker drag events * @param onClick a lambda invoked when the marker is clicked * @param onInfoWindowClick a lambda invoked when the marker's info window is clicked * @param onInfoWindowClose a lambda invoked when the marker's info window is closed @@ -306,7 +320,7 @@ fun MarkerInfoWindowContent( */ @Composable private fun MarkerImpl( - position: LatLng, + positionState: MarkerPositionState = rememberMarkerPositionState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, @@ -319,7 +333,6 @@ private fun MarkerImpl( title: String? = null, visible: Boolean = true, zIndex: Float = 0.0f, - markerDragState: MarkerDragState? = null, onClick: (Marker) -> Boolean = { false }, onInfoWindowClick: (Marker) -> Unit = {}, onInfoWindowClose: (Marker) -> Unit = {}, @@ -338,7 +351,7 @@ private fun MarkerImpl( flat(flat) icon(icon) infoWindowAnchor(infoWindowAnchor.x, infoWindowAnchor.y) - position(position) + position(positionState.position) rotation(rotation) snippet(snippet) title(title) @@ -349,7 +362,7 @@ private fun MarkerImpl( MarkerNode( compositionContext = compositionContext, marker = marker, - markerDragState = markerDragState, + markerPositionState = positionState, onMarkerClick = onClick, onInfoWindowClick = onInfoWindowClick, onInfoWindowClose = onInfoWindowClose, @@ -359,7 +372,6 @@ private fun MarkerImpl( ) }, update = { - update(markerDragState) { this.markerDragState = it } update(onClick) { this.onMarkerClick = it } update(onInfoWindowClick) { this.onInfoWindowClick = it } update(onInfoWindowClose) { this.onInfoWindowClose = it } @@ -373,7 +385,7 @@ private fun MarkerImpl( set(flat) { this.marker.isFlat = it } set(icon) { this.marker.setIcon(it) } set(infoWindowAnchor) { this.marker.setInfoWindowAnchor(it.x, it.y) } - set(position) { this.marker.position = it } + set(positionState.position) { this.marker.position = it } set(rotation) { this.marker.rotation = it } set(snippet) { this.marker.snippet = it