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 40d30ffe..46b72c21 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 @@ -14,22 +14,17 @@ package com.google.maps.android.compose -import androidx.activity.ComponentActivity 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.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue +import org.junit.Assert.* import org.junit.Before import org.junit.Rule import org.junit.Test @@ -42,8 +37,6 @@ class GoogleMapViewTests { private val startingZoom = 10f private val startingPosition = LatLng(1.23, 4.56) - private val assertRoundingError = 0.01 - private lateinit var cameraPositionState: CameraPositionState private fun initMap(content: @Composable () -> Unit = {}) { @@ -97,7 +90,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(1000) { cameraPositionState.isMoving } - composeTestRule.waitUntil(3000) { + composeTestRule.waitUntil(5000) { !cameraPositionState.isMoving } assertFalse(cameraPositionState.isMoving) @@ -238,9 +231,4 @@ class GoogleMapViewTests { assertionBlock() } - - private fun LatLng.assertEquals(other: LatLng) { - assertEquals(latitude, other.latitude, assertRoundingError) - assertEquals(longitude, other.longitude, assertRoundingError) - } } \ No newline at end of file diff --git a/app/src/androidTest/java/com/google/maps/android/compose/MapInColumnTests.kt b/app/src/androidTest/java/com/google/maps/android/compose/MapInColumnTests.kt new file mode 100644 index 00000000..e944944c --- /dev/null +++ b/app/src/androidTest/java/com/google/maps/android/compose/MapInColumnTests.kt @@ -0,0 +1,141 @@ +// Copyright 2022 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 + +import android.util.Log +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import org.junit.Assert.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +private const val TAG = "MapInColumnTests" + +class MapInColumnTests { + @get:Rule + val composeTestRule = createComposeRule() + + private val startingZoom = 10f + private val startingPosition = LatLng(1.23, 4.56) + private lateinit var cameraPositionState: CameraPositionState + + private fun initMap(content: @Composable () -> Unit = {}) { + val countDownLatch = CountDownLatch(1) + composeTestRule.setContent { + var scrollingEnabled by remember { mutableStateOf(true) } + + LaunchedEffect(cameraPositionState.isMoving) { + if (!cameraPositionState.isMoving) { + scrollingEnabled = true + Log.d(TAG, "Map camera stopped moving - Enabling column scrolling...") + } + } + + MapInColumn( + modifier = Modifier.fillMaxSize(), + cameraPositionState, + columnScrollingEnabled = scrollingEnabled, + onMapTouched = { + scrollingEnabled = false + Log.d( + TAG, + "User touched map - Disabling column scrolling after user touched this Box..." + ) + }, + onMapLoaded = { + countDownLatch.countDown() + } + ) + } + 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 testLatLngInVisibleRegion() { + initMap() + composeTestRule.runOnUiThread { + val projection = cameraPositionState.projection + assertNotNull(projection) + assertTrue( + projection!!.visibleRegion.latLngBounds.contains(startingPosition) + ) + } + } + + @Test + fun testLatLngNotInVisibleRegion() { + initMap() + composeTestRule.runOnUiThread { + val projection = cameraPositionState.projection + assertNotNull(projection) + val latLng = LatLng(23.4, 25.6) + assertFalse( + projection!!.visibleRegion.latLngBounds.contains(latLng) + ) + } + } + + @Test + fun testScrollColumn_MapCameraRemainsSame() { + initMap() + // Check that the column scrolls to the last item + composeTestRule.onRoot().performTouchInput { swipeUp() } + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("Item 1").assertIsNotDisplayed() + + // Check that the map didn't change + startingPosition.assertEquals(cameraPositionState.position.target) + } + +// @Test +// fun testPanMapUp_MapCameraChangesColumnDoesNotScroll() { +// initMap() +// // Swipe the map up +// // FIXME - for some reason this scrolls the entire column instead of just the map +// composeTestRule.onNodeWithTag("Map").performTouchInput { swipeUp() } +// composeTestRule.waitForIdle() +// +// // Make sure that the map changed (i.e., we can scroll the map in the column) +// startingPosition.assertNotEquals(cameraPositionState.position.target) +// +// // Check to make sure column didn't scroll +// composeTestRule.onNodeWithTag("Item 1").assertIsDisplayed() +// } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt b/app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt new file mode 100644 index 00000000..9ff769a6 --- /dev/null +++ b/app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt @@ -0,0 +1,17 @@ +package com.google.maps.android.compose + +import com.google.android.gms.maps.model.LatLng +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals + +const val assertRoundingError: Double = 0.01 + +fun LatLng.assertEquals(other: LatLng) { + assertEquals(latitude, other.latitude, assertRoundingError) + assertEquals(longitude, other.longitude, assertRoundingError) +} + +fun LatLng.assertNotEquals(other: LatLng) { + assertNotEquals(latitude, other.latitude, assertRoundingError) + assertNotEquals(longitude, other.longitude, assertRoundingError) +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fa396824..0120d514 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,17 +33,22 @@ android:value="${MAPS_API_KEY}" /> + + - \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt b/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt similarity index 88% rename from app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt rename to app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt index 75fd7a54..d8871816 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapSampleActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt @@ -24,28 +24,9 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.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 -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Switch -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag @@ -57,14 +38,14 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Marker import kotlinx.coroutines.launch -private const val TAG = "MapSampleActivity" +private const val TAG = "BasicMapActivity" -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) +private val singapore = LatLng(1.35, 103.87) +private val singapore2 = LatLng(1.40, 103.77) +private val singapore3 = LatLng(1.45, 103.77) +private val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f) -class MapSampleActivity : ComponentActivity() { +class BasicMapActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) 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 new file mode 100644 index 00000000..02c49847 --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt @@ -0,0 +1,74 @@ +// Copyright 2021 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 + +import android.content.Intent +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp + +private const val TAG = "MapSampleActivity" + +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colors.background + ) { + val context = LocalContext.current + Column( + Modifier + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.padding(10.dp)) + Text( + text = getString(R.string.main_activity_title), + style = MaterialTheme.typography.h5 + ) + Spacer(modifier = Modifier.padding(10.dp)) + Button( + onClick = { + context.startActivity(Intent(context, BasicMapActivity::class.java)) + }) { + Text(getString(R.string.basic_map_activity)) + } + Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, MapInColumnActivity::class.java)) + }) { + Text(getString(R.string.map_in_column_activity)) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt b/app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt new file mode 100644 index 00000000..8aad53c2 --- /dev/null +++ b/app/src/main/java/com/google/maps/android/compose/MapInColumnActivity.kt @@ -0,0 +1,220 @@ +// Copyright 2021 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 + +import android.os.Bundle +import android.util.Log +import android.view.MotionEvent +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Marker + +private const val TAG = "ScrollingMapActivity" + +private val singapore = LatLng(1.35, 103.87) +private val defaultCameraPosition = CameraPosition.fromLatLngZoom(singapore, 11f) + +class MapInColumnActivity : ComponentActivity() { + + @OptIn(ExperimentalComposeUiApi::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + // Observing and controlling the camera's state can be done with a CameraPositionState + val cameraPositionState = rememberCameraPositionState { + position = defaultCameraPosition + } + var columnScrollingEnabled by remember { mutableStateOf(true) } + + // Use a LaunchedEffect keyed on the camera moving state to enable column scrolling when the camera stops moving + LaunchedEffect(cameraPositionState.isMoving) { + if (!cameraPositionState.isMoving) { + columnScrollingEnabled = true + Log.d(TAG, "Map camera stopped moving - Enabling column scrolling...") + } + } + + MapInColumn( + modifier = Modifier.fillMaxSize(), + cameraPositionState, + columnScrollingEnabled = columnScrollingEnabled, + onMapTouched = { + columnScrollingEnabled = false + Log.d( + TAG, + "User touched map - Disabling column scrolling after user touched this Box..." + ) + }, + onMapLoaded = { } + ) + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun MapInColumn( + modifier: Modifier = Modifier, + cameraPositionState: CameraPositionState, + columnScrollingEnabled: Boolean, + onMapTouched: () -> Unit, + onMapLoaded: () -> Unit, +) { + Surface( + modifier = modifier, + color = MaterialTheme.colors.background + ) { + var isMapLoaded by remember { mutableStateOf(false) } + + Column( + Modifier + .fillMaxSize() + .verticalScroll( + rememberScrollState(), + columnScrollingEnabled + ), + horizontalAlignment = Alignment.Start + ) { + Spacer(modifier = Modifier.padding(10.dp)) + for (i in 1..20) { + Text( + text = "Item $i", + modifier = Modifier + .padding(start = 10.dp) + .testTag("Item $i") + ) + } + Spacer(modifier = Modifier.padding(10.dp)) + + Box( + Modifier + .fillMaxWidth() + .height(200.dp) + ) { + GoogleMapViewInColumn( + modifier = Modifier + .fillMaxSize() + .testTag("Map") + .pointerInteropFilter( + onTouchEvent = { + when (it.action) { + MotionEvent.ACTION_DOWN -> { + onMapTouched() + false + } + else -> { + Log.d( + TAG, + "MotionEvent ${it.action} - this never triggers." + ) + true + } + } + } + ), + cameraPositionState = cameraPositionState, + onMapLoaded = { + isMapLoaded = true + onMapLoaded() + }, + ) + if (!isMapLoaded) { + androidx.compose.animation.AnimatedVisibility( + modifier = Modifier + .fillMaxSize(), + visible = !isMapLoaded, + enter = EnterTransition.None, + exit = fadeOut() + ) { + CircularProgressIndicator( + modifier = Modifier + .background(MaterialTheme.colors.background) + .wrapContentSize() + ) + } + } + } + Spacer(modifier = Modifier.padding(10.dp)) + for (i in 21..40) { + Text( + text = "Item $i", + modifier = Modifier + .padding(start = 10.dp) + .testTag("Item $i") + ) + } + Spacer(modifier = Modifier.padding(10.dp)) + } + } +} + +@Composable +private fun GoogleMapViewInColumn( + modifier: Modifier, + cameraPositionState: CameraPositionState, + onMapLoaded: () -> Unit, +) { + val singaporeState = rememberMarkerState(position = singapore) + + var uiSettings by remember { mutableStateOf(MapUiSettings(compassEnabled = false)) } + var mapProperties by remember { + mutableStateOf(MapProperties(mapType = MapType.NORMAL)) + } + + GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState, + properties = mapProperties, + uiSettings = uiSettings, + onMapLoaded = onMapLoaded + ) { + // 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 = "Singapore", + onClick = markerClick, + draggable = true, + ) { + Text(it.title ?: "Title", color = Color.Red) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 24606631..c8fb093c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,4 +16,7 @@ android-maps-compose + "Maps Compose Demos \uD83D\uDDFA" + Basic Map Activity + Map In Column Activity \ No newline at end of file