diff --git a/README.md b/README.md index 63cfaf9b..fb4cb6ae 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,14 @@ You no longer need to specify the Maps SDK for Android or its Utility Library as ```groovy dependencies { - implementation 'com.google.maps.android:maps-compose:3.1.1' + implementation 'com.google.maps.android:maps-compose:4.0.0' - // Optionally, you can include the Compose utils library for Clustering, etc. - implementation 'com.google.maps.android:maps-compose-utils:3.1.1' + // Optionally, you can include the Compose utils library for Clustering, + // Street View metadata checks, etc. + implementation 'com.google.maps.android:maps-compose-utils:4.0.0' // Optionally, you can include the widgets library for ScaleBar, etc. - implementation 'com.google.maps.android:maps-compose-widgets:3.1.1' + implementation 'com.google.maps.android:maps-compose-widgets:4.0.0' } ``` @@ -236,7 +237,16 @@ MarkerInfoWindow( ### Street View You can add a Street View given a location using the `StreetView` composable. -To use it, provide a `StreetViewPanoramaOptions` object as follows: + +1. Test whether a Street View location is valid with the the +`fetchStreetViewData` utility from the [`maps-compose-utils` library](#maps-compose-utility-library). + +```kotlin + streetViewResult = + fetchStreetViewData(singapore, BuildConfig.MAPS_API_KEY) +``` + +2. Once the location is confirmed valid, add a Street View composable by providing a `StreetViewPanoramaOptions` object. ```kotlin val singapore = LatLng(1.3588227, 103.8742114) @@ -270,9 +280,9 @@ GoogleMap( -## Utility Library +## Maps Compose Utility Library -This library also provides optional utilities in the `maps-compose-utils` library. +This library provides optional utilities in the `maps-compose-utils` library from the [Maps SDK for Android Utility Library](https://github.com/googlemaps/android-maps-utils). ### Clustering @@ -295,7 +305,22 @@ Clustering( ) ``` -## Widgets +### Street View metadata utility + +The `fetchStreetViewData` method provides functionality to check whether a location is supported in StreetView. You can avoid errors when adding a Street View panorama to an Android app by calling this metadata utility and only adding a Street View panorama if the response is OK. + +> [!IMPORTANT] +> Be sure to [enable Street View Static API](https://goo.gle/enable-sv-static-api) on the project associated with your API key. + +You can see example usage +in the [`StreetViewActivity`](https://github.com/googlemaps/android-maps-compose/blob/main/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt) of the demo app: + +```kotlin + streetViewResult = + fetchStreetViewData(singapore, BuildConfig.MAPS_API_KEY) +``` + +## Maps Compose Widgets This library also provides optional composable widgets in the `maps-compose-widgets` library that you can use alongside the `GoogleMap` composable. 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 117d991a..707db2ae 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 @@ -80,7 +80,7 @@ class GoogleMapViewTests { initMap() assertEquals(CameraMoveStartedReason.NO_MOVEMENT_YET, cameraPositionState.cameraMoveStartedReason) zoom(shouldAnimate = true, zoomIn = true) { - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(timeout2) { cameraPositionState.isMoving } assertTrue(cameraPositionState.isMoving) @@ -92,10 +92,10 @@ class GoogleMapViewTests { fun testCameraReportsNotMoving() { initMap() zoom(shouldAnimate = true, zoomIn = true) { - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(timeout2) { cameraPositionState.isMoving } - composeTestRule.waitUntil(5000) { + composeTestRule.waitUntil(timeout5) { !cameraPositionState.isMoving } assertFalse(cameraPositionState.isMoving) @@ -106,10 +106,10 @@ class GoogleMapViewTests { fun testCameraZoomInAnimation() { initMap() zoom(shouldAnimate = true, zoomIn = true) { - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(timeout2) { cameraPositionState.isMoving } - composeTestRule.waitUntil(3000) { + composeTestRule.waitUntil(timeout3) { !cameraPositionState.isMoving } assertEquals( @@ -124,10 +124,10 @@ class GoogleMapViewTests { fun testCameraZoomIn() { initMap() zoom(shouldAnimate = false, zoomIn = true) { - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(timeout2) { cameraPositionState.isMoving } - composeTestRule.waitUntil(3000) { + composeTestRule.waitUntil(timeout3) { !cameraPositionState.isMoving } assertEquals( @@ -142,10 +142,10 @@ class GoogleMapViewTests { fun testCameraZoomOut() { initMap() zoom(shouldAnimate = false, zoomIn = false) { - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(timeout2) { cameraPositionState.isMoving } - composeTestRule.waitUntil(3000) { + composeTestRule.waitUntil(timeout3) { !cameraPositionState.isMoving } assertEquals( @@ -160,10 +160,10 @@ class GoogleMapViewTests { fun testCameraZoomOutAnimation() { initMap() zoom(shouldAnimate = true, zoomIn = false) { - composeTestRule.waitUntil(1000) { + composeTestRule.waitUntil(timeout2) { cameraPositionState.isMoving } - composeTestRule.waitUntil(3000) { + composeTestRule.waitUntil(timeout3) { !cameraPositionState.isMoving } assertEquals( diff --git a/app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt b/app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt index 9c575c69..2f73f800 100644 --- a/app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt +++ b/app/src/androidTest/java/com/google/maps/android/compose/StreetViewTests.kt @@ -38,7 +38,7 @@ class StreetViewTests { onClick = onClick ) } - composeTestRule.waitUntil(10000) { + composeTestRule.waitUntil(timeout5) { cameraPositionState.location.position.latitude != 0.0 && cameraPositionState.location.position.longitude != 0.0 } 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 index c8eb5cad..fcb969ee 100644 --- a/app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt +++ b/app/src/androidTest/java/com/google/maps/android/compose/TestUtils.kt @@ -3,6 +3,9 @@ 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 timeout2 = 2_000L +const val timeout3 = 3_000L +const val timeout5 = 5_000L val hasValidApiKey: Boolean = BuildConfig.MAPS_API_KEY.isNotBlank() && BuildConfig.MAPS_API_KEY != "YOUR_API_KEY" diff --git a/app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt b/app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt index 1efae7d7..525d67c9 100644 --- a/app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/CustomControlsActivity.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp + class CustomControlsActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -46,7 +47,9 @@ class CustomControlsActivity : ComponentActivity() { setContent { var isMapLoaded by remember { mutableStateOf(false) } - val mapProperties by remember { mutableStateOf(MapProperties(isMyLocationEnabled = true)) } + // This needs to be manually deactivated to avoid having a custom and the native + // location button + val uiSettings by remember { mutableStateOf(MapUiSettings(myLocationButtonEnabled = false)) } // Observing and controlling the camera's state can be done with a CameraPositionState val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition @@ -59,17 +62,16 @@ class CustomControlsActivity : ComponentActivity() { onMapLoaded = { isMapLoaded = true }, - - myLocationButton = { - MapButton( - "This is a custom location button", - onClick = { - Toast.makeText( - this@CustomControlsActivity, - "Click on my location", - Toast.LENGTH_SHORT - ).show() - }) + uiSettings = uiSettings, + ) + MapButton( + "This is a custom location button", + onClick = { + Toast.makeText( + this@CustomControlsActivity, + "Click on my location", + Toast.LENGTH_SHORT + ).show() }) if (!isMapLoaded) { diff --git a/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt b/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt index d344318e..f190b207 100644 --- a/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/StreetViewActivity.kt @@ -40,21 +40,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.google.android.gms.maps.StreetViewPanoramaOptions +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.Status import com.google.maps.android.compose.streetview.StreetView import com.google.maps.android.compose.streetview.rememberStreetViewCameraPositionState import com.google.maps.android.ktx.MapsExperimentalFeature import kotlinx.coroutines.launch +import com.google.maps.android.StreetViewUtils.Companion.fetchStreetViewData class StreetViewActivity : ComponentActivity() { private val TAG = StreetViewActivity::class.java.simpleName + // This is an invalid location. If you use it instead of Singapore, the StreetViewUtils + // will return NOT_FOUND. + val invalidLocation = LatLng(32.429634, -96.828891) + @OptIn(MapsExperimentalFeature::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + setContent { var isPanningEnabled by remember { mutableStateOf(false) } var isZoomEnabled by remember { mutableStateOf(false) } + var streetViewResult by remember { mutableStateOf(Status.NOT_FOUND) } + val camera = rememberStreetViewCameraPositionState() LaunchedEffect(camera) { launch { @@ -69,41 +79,52 @@ class StreetViewActivity : ComponentActivity() { Log.d(TAG, "Location at: $it") } } + launch { + // Be sure to enable the Street View Static API on the project associated with + // this API key using the instructions at https://goo.gle/enable-sv-static-api + streetViewResult = + fetchStreetViewData(singapore, BuildConfig.MAPS_API_KEY) + } } Box(Modifier.fillMaxSize(), Alignment.BottomStart) { - StreetView( - Modifier.matchParentSize(), - cameraPositionState = camera, - streetViewPanoramaOptionsFactory = { - StreetViewPanoramaOptions().position(singapore) - }, - isPanningGesturesEnabled = isPanningEnabled, - isZoomGesturesEnabled = isZoomEnabled, - onClick = { - Log.d(TAG, "Street view clicked") - }, - onLongClick = { - Log.d(TAG, "Street view long clicked") - } - ) - Column( - Modifier - .fillMaxWidth() - .background(Color.White) - .padding(8.dp) - ) { - StreetViewSwitch(title = "Panning", checked = isPanningEnabled) { - isPanningEnabled = it - } - StreetViewSwitch(title = "Zooming", checked = isZoomEnabled) { - isZoomEnabled = it + if (streetViewResult == Status.OK) { + StreetView( + Modifier.matchParentSize(), + cameraPositionState = camera, + streetViewPanoramaOptionsFactory = { + StreetViewPanoramaOptions().position(singapore) + }, + isPanningGesturesEnabled = isPanningEnabled, + isZoomGesturesEnabled = isZoomEnabled, + onClick = { + Log.d(TAG, "Street view clicked") + }, + onLongClick = { + Log.d(TAG, "Street view long clicked") + } + ) + Column( + Modifier + .fillMaxWidth() + .background(Color.White) + .padding(8.dp) + ) { + StreetViewSwitch(title = "Panning", checked = isPanningEnabled) { + isPanningEnabled = it + } + StreetViewSwitch(title = "Zooming", checked = isZoomEnabled) { + isZoomEnabled = it + } } + } else { + Text("Location not available.") } } } } } + @Composable fun StreetViewSwitch(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { Row(Modifier.padding(4.dp)) { diff --git a/build.gradle b/build.gradle index f2e3bec6..98be586a 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ ext.projectArtifactId = { project -> allprojects { group = 'com.google.maps.android' - version = '3.1.1' + version = '4.0.0' project.ext.artifactId = rootProject.ext.projectArtifactId(project) } diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt index da27b122..8aae82d1 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt @@ -20,7 +20,6 @@ import android.location.Location import android.os.Bundle import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition import androidx.compose.runtime.CompositionContext @@ -88,7 +87,6 @@ public fun GoogleMap( onMyLocationClick: ((Location) -> Unit)? = null, onPOIClick: ((PointOfInterest) -> Unit)? = null, contentPadding: PaddingValues = NoPadding, - myLocationButton: (@Composable @GoogleMapComposable () -> Unit)? = null, content: (@Composable @GoogleMapComposable () -> Unit)? = null, ) { // When in preview, early return a Box with the received modifier preserving layout @@ -119,16 +117,11 @@ public fun GoogleMap( val currentContentPadding by rememberUpdatedState(contentPadding) // If we pass a custom location button, the native one is deactivated. - val currentUiSettings by rememberUpdatedState(if (myLocationButton != null) { - uiSettings.copy(myLocationButtonEnabled = false) - } else { - uiSettings - }) + val currentUiSettings by rememberUpdatedState(uiSettings) val currentMapProperties by rememberUpdatedState(properties) val parentComposition = rememberCompositionContext() val currentContent by rememberUpdatedState(content) - val currentLocation by rememberUpdatedState(myLocationButton) LaunchedEffect(Unit) { disposingComposition { @@ -150,10 +143,6 @@ public fun GoogleMap( } } } - Row(modifier = modifier) { - currentLocation?.invoke() - } - } internal suspend inline fun disposingComposition(factory: () -> Composition) { diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt index cc774f4c..e73c7d53 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/streetview/StreetView.kt @@ -1,3 +1,17 @@ +// Copyright 2023 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.streetview import android.content.ComponentCallbacks @@ -23,14 +37,17 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import com.google.android.gms.maps.StreetViewPanoramaOptions import com.google.android.gms.maps.StreetViewPanoramaView -import com.google.android.gms.maps.model.StreetViewPanoramaCamera import com.google.android.gms.maps.model.StreetViewPanoramaOrientation import com.google.maps.android.compose.disposingComposition import com.google.maps.android.ktx.MapsExperimentalFeature import com.google.maps.android.ktx.awaitStreetViewPanorama /** - * A composable for displaying a Street View for a given location. + * A composable for displaying a Street View for a given location. A location might not be available for a given + * set of coordinates. We recommend you to check our sample on [StreetViewActivity] using our utility function + * in [StreetViewUtils] to manage non-existing locations. + * + * * * @param modifier Modifier to be applied to the StreetView * @param cameraPositionState the [StreetViewCameraPositionState] to be used to control or observe