diff --git a/README.md b/README.md index 332a5941..53d18aa7 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,14 @@ composable elements to the content of the `GoogleMap`. GoogleMap( //... ) { - Marker(position = LatLng(-34, 151), title = "Marker in Sydney") - Marker(position = LatLng(35.66, 139.6), title = "Marker in Tokyo") + Marker( + state = MarkerState(position = LatLng(-34, 151)), + title = "Marker in Sydney" + ) + Marker( + state = MarkerState(position = LatLng(35.66, 139.6)), + title = "Marker in Tokyo" + ) } ``` diff --git a/app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt b/app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt index eb61fdd2..80ee4a3a 100644 --- a/app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt +++ b/app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt @@ -101,7 +101,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving } - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(3000) { !cameraPositionState.isMoving } assertEquals( @@ -118,7 +118,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving } - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(3000) { !cameraPositionState.isMoving } assertEquals( @@ -135,7 +135,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving } - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(3000) { !cameraPositionState.isMoving } assertEquals( @@ -184,6 +184,30 @@ class GoogleMapViewTests { ) } + @Test(expected = IllegalStateException::class) + fun testMarkerStateCannotBeReused() { + val countDownLatch = CountDownLatch(1) + composeTestRule.setContent { + GoogleMap( + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState, + onMapLoaded = { + countDownLatch.countDown() + } + ) { + val markerState = rememberMarkerState() + Marker( + state = markerState + ) + Marker( + state = markerState + ) + } + } + val mapLoaded = countDownLatch.await(30, TimeUnit.SECONDS) + assertTrue(mapLoaded) + } + private fun zoom( shouldAnimate: Boolean, zoomIn: Boolean, 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 87936ebe..36051f23 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 @@ -60,6 +62,7 @@ private const val TAG = "MapSampleActivity" val singapore = LatLng(1.35, 103.87) val singapore2 = LatLng(1.40, 103.77) val singapore3 = LatLng(1.45, 103.77) +val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f) class MapSampleActivity : ComponentActivity() { @@ -69,7 +72,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 +109,14 @@ fun GoogleMapView( cameraPositionState: CameraPositionState, onMapLoaded: () -> Unit, ) { + val singaporeState = rememberMarkerState(position = singapore) + val singapore2State = rememberMarkerState(position = singapore2) + val singapore3State = rememberMarkerState(position = singapore3) + var circleCenter by remember { mutableStateOf(singapore) } + if (singaporeState.dragState == DragState.END) { + circleCenter = singaporeState.position + } + var uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) } var shouldAnimateZoom by remember { mutableStateOf(true) } var ticker by remember { mutableStateOf(0) } @@ -132,14 +143,15 @@ fun GoogleMapView( false } MarkerInfoWindowContent( - position = singapore, + state = singaporeState, title = "Zoom in has been tapped $ticker times.", onClick = markerClick, + draggable = true, ) { Text(it.title ?: "Title", color = Color.Red) } MarkerInfoWindowContent( - position = singapore2, + state = singapore2State, title = "Marker with custom info window.\nZoom in has been tapped $ticker times.", icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE), onClick = markerClick, @@ -147,12 +159,12 @@ fun GoogleMapView( Text(it.title ?: "Title", color = Color.Blue) } Marker( - position = singapore3, + state = singapore3State, title = "Marker in Singapore", onClick = markerClick ) Circle( - center = singapore, + center = circleCenter, fillColor = MaterialTheme.colors.secondary, strokeColor = MaterialTheme.colors.secondaryVariant, radius = 1000.0, @@ -164,6 +176,21 @@ 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 + singaporeState.position = singapore + singaporeState.hideInfoWindow() + } + ) { + Text(text = "RESET MAP", style = MaterialTheme.typography.body1) + } val coroutineScope = rememberCoroutineScope() ZoomControls( shouldAnimateZoom, @@ -194,7 +221,7 @@ fun GoogleMapView( uiSettings = uiSettings.copy(zoomControlsEnabled = it) } ) - DebugView(cameraPositionState) + DebugView(cameraPositionState, singaporeState) } } @@ -271,7 +298,10 @@ private fun MapButton(text: String, onClick: () -> Unit) { } @Composable -private fun DebugView(cameraPositionState: CameraPositionState) { +private fun DebugView( + cameraPositionState: CameraPositionState, + markerState: MarkerState +) { Column( Modifier .fillMaxWidth(), @@ -281,5 +311,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 (markerState.dragState == DragState.DRAG) "dragging" else "not dragging" + Text(text = "Marker is $dragging") + Text(text = "Marker position is ${markerState.position}") } -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index c466d5fe..c1d45c23 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ ext.projectArtifactId = { project -> allprojects { group = 'com.google.maps.android' - version = '1.3.1' + version = '2.0.0-SNAPSHOT' project.ext.artifactId = rootProject.ext.projectArtifactId(project) } @@ -125,8 +125,10 @@ subprojects { project -> repositories { maven { + def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/" name = "mavencentral" - url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + url = project.version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl credentials { username sonatypeUsername password sonatypePassword 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 3144268e..09b68361 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 @@ -111,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?.markerState?.position = marker.position + this?.markerState?.dragState = DragState.DRAG + } } override fun onMarkerDragEnd(marker: Marker) { - val markerDragState = - decorations.nodeForMarker(marker)?.markerDragState - markerDragState?.dragState = DragState.END + with(decorations.nodeForMarker(marker)) { + this?.markerState?.position = marker.position + this?.markerState?.dragState = DragState.END + } } override fun onMarkerDragStart(marker: Marker) { - val markerDragState = - decorations.nodeForMarker(marker)?.markerDragState - markerDragState?.dragState = DragState.START + with(decorations.nodeForMarker(marker)) { + this?.markerState?.position = marker.position + this?.markerState?.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..920c362f 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 markerState: MarkerState, var onMarkerClick: (Marker) -> Boolean, var onInfoWindowClick: (Marker) -> Unit, var onInfoWindowClose: (Marker) -> Unit, @@ -41,7 +42,11 @@ internal class MarkerNode( var infoWindow: (@Composable (Marker) -> Unit)?, var infoContent: (@Composable (Marker) -> Unit)?, ) : MapNode { + override fun onAttached() { + markerState.marker = marker + } override fun onRemoved() { + markerState.marker = null marker.remove() } } @@ -52,28 +57,72 @@ 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 MarkerState( + 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 + + // The marker associated with this MarkerState. + internal var marker: Marker? = null + set(value) { + if (field == null && value == null) return + if (field != null && value != null) { + error("MarkerState may only be associated with one Marker at a time.") + } + field = value + } + + /** + * Shows the info window for the underlying marker + */ + fun showInfoWindow() { + marker?.showInfoWindow() + } + + /** + * Hides the info window for the underlying marker + */ + fun hideInfoWindow() { + marker?.hideInfoWindow() + } + + companion object { + /** + * The default saver implementation for [MarkerState] + */ + val Saver = Saver( + save = { it.position }, + restore = { MarkerState(it) } + ) + } } -/** - * Creates and [remember] a [MarkerDragState]. - */ @Composable -fun rememberMarkerDragState(): MarkerDragState = remember { - MarkerDragState() +fun rememberMarkerState( + key: String? = null, + position: LatLng = LatLng(0.0, 0.0) +): MarkerState = rememberSaveable(key = key, saver = MarkerState.Saver) { + MarkerState(position) } /** * A composable for a marker on the map. * - * @param position the position of the marker + * @param state the [MarkerState] to be used to control or observe the marker + * state such as its position and info window * @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 +135,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 +142,7 @@ fun rememberMarkerDragState(): MarkerDragState = remember { */ @Composable fun Marker( - position: LatLng, + state: MarkerState = rememberMarkerState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, @@ -107,14 +155,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, + state = state, alpha = alpha, anchor = anchor, draggable = draggable, @@ -127,7 +174,6 @@ fun Marker( title = title, visible = visible, zIndex = zIndex, - markerDragState = markerDragState, onClick = onClick, onInfoWindowClick = onInfoWindowClick, onInfoWindowClose = onInfoWindowClose, @@ -140,7 +186,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 state the [MarkerState] to be used to control or observe the marker + * state such as its position and info window * @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 +200,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 +209,7 @@ fun Marker( */ @Composable fun MarkerInfoWindow( - position: LatLng, + state: MarkerState = rememberMarkerState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, @@ -176,7 +222,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 +229,7 @@ fun MarkerInfoWindow( content: (@Composable (Marker) -> Unit)? = null ) { MarkerImpl( - position = position, + state = state, alpha = alpha, anchor = anchor, draggable = draggable, @@ -197,7 +242,6 @@ fun MarkerInfoWindow( title = title, visible = visible, zIndex = zIndex, - markerDragState = markerDragState, onClick = onClick, onInfoWindowClick = onInfoWindowClick, onInfoWindowClose = onInfoWindowClose, @@ -211,7 +255,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 state the [MarkerState] to be used to control or observe the marker + * state such as its position and info window * @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 +269,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 +278,7 @@ fun MarkerInfoWindow( */ @Composable fun MarkerInfoWindowContent( - position: LatLng, + state: MarkerState = rememberMarkerState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, @@ -247,7 +291,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 +298,7 @@ fun MarkerInfoWindowContent( content: (@Composable (Marker) -> Unit)? = null ) { MarkerImpl( - position = position, + state = state, alpha = alpha, anchor = anchor, draggable = draggable, @@ -268,7 +311,6 @@ fun MarkerInfoWindowContent( title = title, visible = visible, zIndex = zIndex, - markerDragState = markerDragState, onClick = onClick, onInfoWindowClick = onInfoWindowClick, onInfoWindowClose = onInfoWindowClose, @@ -280,7 +322,8 @@ fun MarkerInfoWindowContent( /** * Internal implementation for a marker on a Google map. * - * @param position the position of the marker + * @param state the [MarkerState] to be used to control or observe the marker + * state such as its position and info window * @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 +336,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 +348,7 @@ fun MarkerInfoWindowContent( */ @Composable private fun MarkerImpl( - position: LatLng, + state: MarkerState = rememberMarkerState(), alpha: Float = 1.0f, anchor: Offset = Offset(0.5f, 1.0f), draggable: Boolean = false, @@ -319,7 +361,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 +379,7 @@ private fun MarkerImpl( flat(flat) icon(icon) infoWindowAnchor(infoWindowAnchor.x, infoWindowAnchor.y) - position(position) + position(state.position) rotation(rotation) snippet(snippet) title(title) @@ -349,7 +390,7 @@ private fun MarkerImpl( MarkerNode( compositionContext = compositionContext, marker = marker, - markerDragState = markerDragState, + markerState = state, onMarkerClick = onClick, onInfoWindowClick = onInfoWindowClick, onInfoWindowClose = onInfoWindowClose, @@ -359,7 +400,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 +413,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(state.position) { this.marker.position = it } set(rotation) { this.marker.rotation = it } set(snippet) { this.marker.snippet = it