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 80ee4a3a..b6ed2b69 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 @@ -15,13 +15,13 @@ package com.google.maps.android.compose import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick -import androidx.test.annotation.UiThreadTest import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import org.junit.Assert.assertEquals @@ -44,15 +44,7 @@ class GoogleMapViewTests { private lateinit var cameraPositionState: CameraPositionState - @Before - fun setUp() { - cameraPositionState = CameraPositionState( - position = CameraPosition.fromLatLngZoom( - startingPosition, - startingZoom - ) - ) - + private fun initMap(content: @Composable () -> Unit = {}) { val countDownLatch = CountDownLatch(1) composeTestRule.setContent { GoogleMapView( @@ -61,19 +53,33 @@ class GoogleMapViewTests { onMapLoaded = { countDownLatch.countDown() } - ) + ) { + content.invoke() + } } val mapLoaded = countDownLatch.await(30, TimeUnit.SECONDS) assertTrue("Map loaded", mapLoaded) } + @Before + fun setUp() { + cameraPositionState = CameraPositionState( + position = CameraPosition.fromLatLngZoom( + startingPosition, + startingZoom + ) + ) + } + @Test fun testStartingCameraPosition() { + initMap() startingPosition.assertEquals(cameraPositionState.position.target) } @Test fun testCameraReportsMoving() { + initMap() zoom(shouldAnimate = true, zoomIn = true) { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving @@ -84,6 +90,7 @@ class GoogleMapViewTests { @Test fun testCameraReportsNotMoving() { + initMap() zoom(shouldAnimate = true, zoomIn = true) { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving @@ -97,6 +104,7 @@ class GoogleMapViewTests { @Test fun testCameraZoomInAnimation() { + initMap() zoom(shouldAnimate = true, zoomIn = true) { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving @@ -114,6 +122,7 @@ class GoogleMapViewTests { @Test fun testCameraZoomIn() { + initMap() zoom(shouldAnimate = false, zoomIn = true) { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving @@ -131,6 +140,7 @@ class GoogleMapViewTests { @Test fun testCameraZoomOut() { + initMap() zoom(shouldAnimate = false, zoomIn = false) { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving @@ -148,6 +158,7 @@ class GoogleMapViewTests { @Test fun testCameraZoomOutAnimation() { + initMap() zoom(shouldAnimate = true, zoomIn = false) { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving @@ -164,48 +175,49 @@ class GoogleMapViewTests { } @Test - @UiThreadTest fun testLatLngInVisibleRegion() { - val projection = cameraPositionState.projection - assertNotNull(projection) - assertTrue( - projection!!.visibleRegion.latLngBounds.contains(startingPosition) - ) + initMap() + composeTestRule.runOnUiThread { + val projection = cameraPositionState.projection + assertNotNull(projection) + assertTrue( + projection!!.visibleRegion.latLngBounds.contains(startingPosition) + ) + } } @Test - @UiThreadTest fun testLatLngNotInVisibleRegion() { - val projection = cameraPositionState.projection - assertNotNull(projection) - val latLng = LatLng(23.4, 25.6) - assertFalse( - projection!!.visibleRegion.latLngBounds.contains(latLng) - ) + initMap() + composeTestRule.runOnUiThread { + val projection = cameraPositionState.projection + assertNotNull(projection) + val latLng = LatLng(23.4, 25.6) + assertFalse( + projection!!.visibleRegion.latLngBounds.contains(latLng) + ) + } } @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 - ) - } + initMap { + val markerState = rememberMarkerState() + Marker( + state = markerState + ) + Marker( + state = markerState + ) } - val mapLoaded = countDownLatch.await(30, TimeUnit.SECONDS) - assertTrue(mapLoaded) + } + + @Test + fun testCameraPositionStateMapClears() { + initMap() + composeTestRule.onNodeWithTag("toggleMapVisibility") + .performClick() + .performClick() } private fun zoom( 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 36051f23..75fd7a54 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 @@ -108,6 +108,7 @@ fun GoogleMapView( modifier: Modifier, cameraPositionState: CameraPositionState, onMapLoaded: () -> Unit, + content: @Composable () -> Unit = {} ) { val singaporeState = rememberMarkerState(position = singapore) val singapore2State = rememberMarkerState(position = singapore2) @@ -123,73 +124,78 @@ fun GoogleMapView( var mapProperties by remember { mutableStateOf(MapProperties(mapType = MapType.NORMAL)) } + var mapVisible by remember { mutableStateOf(true) } - GoogleMap( - modifier = modifier, - cameraPositionState = cameraPositionState, - properties = mapProperties, - uiSettings = uiSettings, - onMapLoaded = onMapLoaded, - onPOIClick = { - Log.d(TAG, "POI clicked: ${it.name}") - } - ) { - // Drawing on the map is accomplished with a child-based API - val markerClick: (Marker) -> Boolean = { - Log.d(TAG, "${it.title} was clicked") - cameraPositionState.projection?.let { projection -> - Log.d(TAG, "The current projection is: $projection") + if (mapVisible) { + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState, + properties = mapProperties, + uiSettings = uiSettings, + onMapLoaded = onMapLoaded, + onPOIClick = { + Log.d(TAG, "POI clicked: ${it.name}") } - false - } - MarkerInfoWindowContent( - state = singaporeState, - title = "Zoom in has been tapped $ticker times.", - onClick = markerClick, - draggable = true, - ) { - Text(it.title ?: "Title", color = Color.Red) - } - MarkerInfoWindowContent( - state = singapore2State, - title = "Marker with custom info window.\nZoom in has been tapped $ticker times.", - icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE), - onClick = markerClick, ) { - Text(it.title ?: "Title", color = Color.Blue) + // Drawing on the map is accomplished with a child-based API + val markerClick: (Marker) -> Boolean = { + Log.d(TAG, "${it.title} was clicked") + cameraPositionState.projection?.let { projection -> + Log.d(TAG, "The current projection is: $projection") + } + false + } + MarkerInfoWindowContent( + state = singaporeState, + title = "Zoom in has been tapped $ticker times.", + onClick = markerClick, + draggable = true, + ) { + Text(it.title ?: "Title", color = Color.Red) + } + MarkerInfoWindowContent( + state = singapore2State, + title = "Marker with custom info window.\nZoom in has been tapped $ticker times.", + icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE), + onClick = markerClick, + ) { + Text(it.title ?: "Title", color = Color.Blue) + } + Marker( + state = singapore3State, + title = "Marker in Singapore", + onClick = markerClick + ) + Circle( + center = circleCenter, + fillColor = MaterialTheme.colors.secondary, + strokeColor = MaterialTheme.colors.secondaryVariant, + radius = 1000.0, + ) + content() } - Marker( - state = singapore3State, - title = "Marker in Singapore", - onClick = markerClick - ) - Circle( - center = circleCenter, - fillColor = MaterialTheme.colors.secondary, - strokeColor = MaterialTheme.colors.secondaryVariant, - radius = 1000.0, - ) - } + } Column { MapTypeControls(onMapTypeClick = { 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) + Row { + MapButton( + text = "Reset Map", + onClick = { + mapProperties = mapProperties.copy(mapType = MapType.NORMAL) + cameraPositionState.position = defaultCameraPosition + singaporeState.position = singapore + singaporeState.hideInfoWindow() + } + ) + MapButton( + text = "Toggle Map", + onClick = { mapVisible = !mapVisible }, + modifier = Modifier.testTag("toggleMapVisibility"), + ) } val coroutineScope = rememberCoroutineScope() ZoomControls( @@ -242,18 +248,8 @@ private fun MapTypeControls( } @Composable -private fun MapTypeButton(type: MapType, onClick: () -> Unit) { - Button( - modifier = Modifier.padding(4.dp), - colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.onPrimary, - contentColor = MaterialTheme.colors.primary - ), - onClick = onClick - ) { - Text(text = type.toString(), style = MaterialTheme.typography.body1) - } -} +private fun MapTypeButton(type: MapType, onClick: () -> Unit) = + MapButton(text = type.toString(), onClick = onClick) @Composable private fun ZoomControls( @@ -284,16 +280,16 @@ private fun ZoomControls( } @Composable -private fun MapButton(text: String, onClick: () -> Unit) { +private fun MapButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { Button( - modifier = Modifier.padding(8.dp), + modifier = modifier.padding(4.dp), colors = ButtonDefaults.buttonColors( backgroundColor = MaterialTheme.colors.onPrimary, contentColor = MaterialTheme.colors.primary ), onClick = onClick ) { - Text(text = text, style = MaterialTheme.typography.h5) + Text(text = text, style = MaterialTheme.typography.body1) } } 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 09b68361..2940046d 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 @@ -26,6 +26,7 @@ import com.google.android.gms.maps.model.Polyline internal interface MapNode { fun onAttached() {} fun onRemoved() {} + fun onCleared() {} } private object MapNodeRoot : MapNode @@ -43,6 +44,8 @@ internal class MapApplier( override fun onClear() { map.clear() + decorations.forEach { it.onCleared() } + decorations.clear() } override fun insertBottomUp(index: Int, instance: MapNode) { diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt index 2359cd6f..dca9fb4b 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt @@ -94,6 +94,10 @@ internal class MapPropertiesNode( override fun onRemoved() { cameraPositionState.setMap(null) } + + override fun onCleared() { + cameraPositionState.setMap(null) + } } internal val NoPadding = PaddingValues() 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 920c362f..caf45f4c 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 @@ -49,6 +49,11 @@ internal class MarkerNode( markerState.marker = null marker.remove() } + + override fun onCleared() { + markerState.marker = null + marker.remove() + } } @Immutable