diff --git a/README.md b/README.md index e31848ce6..a39aca159 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' - - // Optionally, you can include the Compose utils library for Clustering, etc. - implementation 'com.google.maps.android:maps-compose-utils:3.1.1' + implementation 'com.google.maps.android:maps-compose:4.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.1.1' // 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.1.1' } ``` @@ -158,38 +159,44 @@ composable elements to the content of the `GoogleMap`. ```kotlin GoogleMap( - //... + googleMapOptionsFactory = { + GoogleMapOptions().mapId("DEMO_MAP_ID") + }, + //... ) { - Marker( + AdvancedMarker( state = MarkerState(position = LatLng(-34, 151)), title = "Marker in Sydney" ) - Marker( + AdvancedMarker( state = MarkerState(position = LatLng(35.66, 139.6)), title = "Marker in Tokyo" ) } ``` -You can also customize the marker you want to add by using `MarkerComposable`. +You can customize a marker by using `PinConfig` with an `AdvancedMarker`. ```kotlin val state = MyState() GoogleMap( - //... + googleMapOptionsFactory = { + GoogleMapOptions().mapId("DEMO_MAP_ID") + }, + //... ) { - MarkerComposable( - keys = arrayOf(state), + val pinConfig = PinConfig.builder() + .setBackgroundColor(Color.MAGENTA) + .build() + + AdvancedMarker( state = MarkerState(position = LatLng(-34, 151)), - ) { - MyCustomMarker(state) - } + title = "Magenta marker in Sydney", + pinConfig = pinConfig + ) } ``` -As this Composable is backed by a rendering of your Composable into a Bitmap, it will not render -your Composable every recomposition. So to trigger a new render of your Composable, you can pass -all variables that your Composable depends on to trigger a render whenever one of them change. @@ -230,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) @@ -264,16 +280,16 @@ 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 The marker clustering utility helps you manage multiple markers at different zoom levels. When a user views the map at a high zoom level, the individual markers show on the map. When the user zooms out, the markers gather together into clusters, to make viewing the map easier. -The [MapClusteringActivity](app/src/main/java/com/google/maps/android/compose/MapClusteringActivity.kt) demonstrates usage. +The [MarkerClusteringActivity](app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt) demonstrates usage. ```kotlin Clustering( @@ -289,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. @@ -304,8 +335,8 @@ The [ScaleBarActivity](app/src/main/java/com/google/maps/android/compose/ScaleBa Both versions of this widget leverage the `CameraPositionState` in `maps-compose` and therefore are very simple to configure with their defaults: ```kotlin -Box(Modifier.fillMaxSize()) { - +Box(Modifier.fillMaxSize()) { + GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState @@ -328,7 +359,7 @@ Box(Modifier.fillMaxSize()) { .align(Alignment.TopStart), cameraPositionState = cameraPositionState ) -} +} ``` The colors of the text, line, and shadow are also all configurable (e.g., based on `isSystemInDarkTheme()` on a dark map). Similarly, the `DisappearingScaleBar` animations can be configured. @@ -339,12 +370,13 @@ Contributions are welcome and encouraged! See [contributing] for more info. ## Support -Encounter an issue while using this library? +This library is offered via an open source [license](LICENSE). It is not governed by the Google Maps Platform [Technical Support Services Guidelines](https://cloud.google.com/maps-platform/terms/tssg?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=android_oss), the [SLA](https://cloud.google.com/maps-platform/terms/sla?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=android_oss), or the [Deprecation Policy](https://cloud.google.com/maps-platform/terms?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=android_oss) (however, any Google Maps Platform services used by the library remain subject to the Google Maps Platform Terms of Service). + +This library adheres to [semantic versioning](https://semver.org/) to indicate when backwards-incompatible changes are introduced. -If you find a bug or have a feature request, please [file an issue]. -Or, if you'd like to contribute, send us a [pull request] and refer to our [code of conduct]. +If you find a bug, or have a feature request, please [file an issue] on GitHub. -You can also discuss this library on our [Discord server]. +If you would like to get answers to technical questions from other Google Maps Platform developers, ask through one of our [developer community channels](https://developers.google.com/maps/developer-community?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=android_oss) including the Google Maps Platform [Discord server]. [maps-sdk]: https://developers.google.com/maps/documentation/android-sdk [api-key]: https://developers.google.com/maps/documentation/android-sdk/get-api-key diff --git a/app/build.gradle b/app/build.gradle index c823a2ee2..d68d545d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,7 +44,6 @@ dependencies { implementation libs.androidx.compose.material implementation libs.kotlin implementation libs.material - implementation libs.maps.ktx.std implementation libs.androidx.compose.ui.preview.tooling debugImplementation libs.androidx.compose.ui.tooling @@ -58,15 +57,11 @@ dependencies { androidTestImplementation libs.androidx.test.compose.ui androidTestImplementation libs.coroutines - // Uncomment the implementation 'com.google...` declaration and comment out the project - // declaration if you want to test the sample app with a Maven Central release of the library. - //implementation "com.google.maps.android:maps-compose:2.2.1" + // Instead of the lines below, regular apps would load these libraries from Maven according to + // the README installation instructions implementation project(':maps-compose') - //implementation "com.google.maps.android:maps-compose-widgets:1.0.0" implementation project(':maps-compose-widgets') - //implementation "com.google.maps.android:maps-compose-utils:1.0.0" implementation project(':maps-compose-utils') - implementation libs.maps.playservice } secrets { 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 117d991a9..707db2ae5 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 9c575c694..2f73f800d 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 c8eb5cad1..fcb969ee0 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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e008f6e42..c436e0f33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,6 +42,9 @@ + @@ -49,7 +52,7 @@ android:name=".MapsInLazyColumnActivity" android:exported="false"/> Boolean = { + Log.d(TAG, "${it.title} was clicked") + cameraPositionState.projection?.let { projection -> + Log.d(TAG, "The current projection is: $projection") + } + false + } + Box(Modifier.fillMaxSize()) { + GoogleMap( + modifier = Modifier.matchParentSize(), + googleMapOptionsFactory = { + GoogleMapOptions().mapId("DEMO_MAP_ID") + }, + cameraPositionState = cameraPositionState, + properties = mapProperties, + onPOIClick = { + Log.d(TAG, "POI clicked: ${it.name}") + } + ) { + + val textView = TextView(this@AdvancedMarkersActivity) + textView.text = "Hello!!" + textView.setBackgroundColor(Color.BLACK) + textView.setTextColor(Color.YELLOW) + + AdvancedMarker( + state = marker4State, + onClick = markerClick, + collisionBehavior = 1, + iconView = textView, + title="Marker 4" + ) + + val pinConfig = PinConfig.builder() + .setBackgroundColor(Color.MAGENTA) + .setBorderColor(Color.WHITE) + .build() + + AdvancedMarker( + state = marker1State, + onClick = markerClick, + collisionBehavior = 1, + pinConfig = pinConfig, + title="Marker 1" + ) + + val glyphOne = PinConfig.Glyph("A", Color.BLACK) + val pinConfig2 = PinConfig.builder() + .setGlyph(glyphOne) + .build() + + AdvancedMarker( + state = marker2State, + onClick = markerClick, + collisionBehavior = 1, + pinConfig = pinConfig2, + title="Marker 2" + ) + + val glyphImage: Int = ic_menu_myplaces + val descriptor = BitmapDescriptorFactory.fromResource(glyphImage) + val pinConfig3 = PinConfig.builder() + .setGlyph(PinConfig.Glyph(descriptor)) + .build() + + AdvancedMarker( + state = marker3State, + onClick = markerClick, + collisionBehavior = 1, + pinConfig = pinConfig3, + title="Marker 3" + ) + + } + } + } + } + + override fun onMapsSdkInitialized(renderer: MapsInitializer.Renderer) { + when (renderer) { + MapsInitializer.Renderer.LATEST -> Log.d("MapsDemo", "The latest version of the renderer is used.") + MapsInitializer.Renderer.LEGACY -> Log.d("MapsDemo", "The legacy version of the renderer is used.") + else -> {} + } + } +} \ No newline at end of file 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 1efae7d76..525d67c90 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/MainActivity.kt b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt index 35a778162..39a3eec76 100644 --- a/app/src/main/java/com/google/maps/android/compose/MainActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt @@ -22,6 +22,8 @@ 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.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface @@ -32,8 +34,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.google.maps.android.compose.theme.MapsComposeSampleTheme -private const val TAG = "MapSampleActivity" - class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -47,7 +47,8 @@ class MainActivity : ComponentActivity() { val context = LocalContext.current Column( Modifier - .fillMaxSize(), + .fillMaxSize() + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.padding(10.dp)) @@ -63,16 +64,23 @@ class MainActivity : ComponentActivity() { Text(getString(R.string.basic_map_activity)) } Spacer(modifier = Modifier.padding(5.dp)) + Button( + onClick = { + context.startActivity(Intent(context, AdvancedMarkersActivity::class.java)) + }) { + Text(getString(R.string.advanced_markers)) + } + Spacer(modifier = Modifier.padding(5.dp)) Button( onClick = { context.startActivity( Intent( context, - MapInColumnActivity::class.java + MarkerClusteringActivity::class.java ) ) }) { - Text(getString(R.string.map_in_column_activity)) + Text(getString(R.string.marker_clustering_activity)) } Spacer(modifier = Modifier.padding(5.dp)) Button( @@ -80,11 +88,11 @@ class MainActivity : ComponentActivity() { context.startActivity( Intent( context, - MapClusteringActivity::class.java + MapInColumnActivity::class.java ) ) }) { - Text(getString(R.string.map_clustering_activity)) + Text(getString(R.string.map_in_column_activity)) } Spacer(modifier = Modifier.padding(5.dp)) Button( diff --git a/app/src/main/java/com/google/maps/android/compose/MapClusteringActivity.kt b/app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt similarity index 97% rename from app/src/main/java/com/google/maps/android/compose/MapClusteringActivity.kt rename to app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt index d545dd8ee..328076c5b 100644 --- a/app/src/main/java/com/google/maps/android/compose/MapClusteringActivity.kt +++ b/app/src/main/java/com/google/maps/android/compose/MarkerClusteringActivity.kt @@ -28,9 +28,9 @@ import com.google.maps.android.clustering.ClusterItem import com.google.maps.android.compose.clustering.Clustering import kotlin.random.Random -private val TAG = MapClusteringActivity::class.simpleName +private val TAG = MarkerClusteringActivity::class.simpleName -class MapClusteringActivity : ComponentActivity() { +class MarkerClusteringActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -60,7 +60,7 @@ fun GoogleMapClustering(items: List) { GoogleMap( modifier = Modifier.fillMaxSize(), cameraPositionState = rememberCameraPositionState { - position = CameraPosition.fromLatLngZoom(singapore, 10f) + position = CameraPosition.fromLatLngZoom(singapore, 6f) } ) { Clustering( 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 d344318e8..f190b2070 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/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 276bd38dd..8c45fb0f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,8 +18,9 @@ android-maps-compose "Maps Compose Demos \uD83D\uDDFA" Basic Map + Advanced Markers Map In Column - Map Clustering + Marker Clustering Location Tracking Scale Bar Street View diff --git a/build.gradle b/build.gradle index f2e3bec67..99957480f 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.1.1' project.ext.artifactId = rootProject.ext.projectArtifactId(project) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a61fc081..b455bf2af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ junitktx = "1.1.5" junit = "4.13.2" kotlin = "1.9.10" material = "1.9.0" -mapsktx = "4.0.0" +mapsktx = "5.0.0" mapsecrets = "2.0.1" # We have to override this version because the current release has a @@ -45,7 +45,6 @@ kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "ko kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } maps-ktx-std = { module = "com.google.maps.android:maps-ktx", version.ref = "mapsktx" } maps-ktx-utils = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "mapsktx" } -maps-playservice = { module = "com.google.android.gms:play-services-maps", version.require = "18.1.0" } maps-secrets-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "mapsecrets" } material = { module = "com.google.android.material:material", version.ref = "material" } test-junit = { module = "junit:junit", version.ref = "junit" } diff --git a/maps-compose-utils/build.gradle b/maps-compose-utils/build.gradle index 2febaa60e..c5ae7898b 100644 --- a/maps-compose-utils/build.gradle +++ b/maps-compose-utils/build.gradle @@ -41,7 +41,5 @@ dependencies { implementation platform(libs.androidx.compose.bom) implementation libs.androidx.compose.ui implementation libs.kotlin - implementation libs.maps.playservice - implementation libs.maps.ktx.std api libs.maps.ktx.utils } diff --git a/maps-compose-widgets/build.gradle b/maps-compose-widgets/build.gradle index ecd945fc8..e36a8d3f1 100644 --- a/maps-compose-widgets/build.gradle +++ b/maps-compose-widgets/build.gradle @@ -42,9 +42,8 @@ dependencies { implementation libs.androidx.compose.material implementation libs.androidx.core implementation libs.kotlin - implementation libs.maps.playservice - implementation libs.maps.ktx.std - implementation libs.maps.ktx.utils + api libs.maps.ktx.std + api libs.maps.ktx.utils testImplementation libs.test.junit androidTestImplementation platform(libs.androidx.compose.bom) diff --git a/maps-compose/build.gradle b/maps-compose/build.gradle index 5da3074a3..e6a8343a2 100644 --- a/maps-compose/build.gradle +++ b/maps-compose/build.gradle @@ -39,8 +39,7 @@ dependencies { implementation libs.androidx.core implementation libs.androidx.compose.foundation implementation libs.kotlin - implementation libs.maps.playservice - implementation libs.maps.ktx.std + api libs.maps.ktx.std testImplementation libs.test.junit 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 63ef9b015..aaa6d252e 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 @@ -89,7 +88,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 @@ -134,16 +132,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 { @@ -165,10 +158,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/Marker.kt b/maps-compose/src/main/java/com/google/maps/android/compose/Marker.kt index 5412e917b..eff2e37e9 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 @@ -14,6 +14,7 @@ package com.google.maps.android.compose +import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.ComposeNode import androidx.compose.runtime.CompositionContext @@ -27,9 +28,12 @@ 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.AdvancedMarkerOptions import com.google.android.gms.maps.model.BitmapDescriptor +import com.google.android.gms.maps.model.BitmapDescriptorFactory import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.PinConfig import com.google.maps.android.ktx.addMarker internal class MarkerNode( @@ -46,6 +50,7 @@ internal class MarkerNode( override fun onAttached() { markerState.marker = marker } + override fun onRemoved() { markerState.marker = null marker.remove() @@ -514,3 +519,204 @@ private fun MarkerImpl( } ) } + + +/** + * A composable for an advanced marker on the map. + * + * @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 + * @param flat sets if the marker should be flat against the map + * @param infoWindowAnchor the anchor point of the info window on the marker image + * @param rotation the rotation of the marker in degrees clockwise about the marker's anchor point + * @param snippet the snippet for the marker + * @param tag optional tag to associate with the marker + * @param title the title for the marker + * @param visible the visibility of the marker + * @param zIndex the z-index of the marker + * @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 + * @param onInfoWindowLongClick a lambda invoked when the marker's info window is long clicked + * @param pinConfig the PinConfig object that will be used for the advanced marker + * @param iconView the custom view to be used on the advanced marker + * @param collisionBehavior the expected collision behavior + */ +@Composable +@GoogleMapComposable +public fun AdvancedMarker( + state: MarkerState = rememberMarkerState(), + alpha: Float = 1.0f, + anchor: Offset = Offset(0.5f, 1.0f), + draggable: Boolean = false, + flat: Boolean = false, + infoWindowAnchor: Offset = Offset(0.5f, 0.0f), + rotation: Float = 0.0f, + snippet: String? = null, + tag: Any? = null, + title: String? = null, + visible: Boolean = true, + zIndex: Float = 0.0f, + onClick: (Marker) -> Boolean = { false }, + onInfoWindowClick: (Marker) -> Unit = {}, + onInfoWindowClose: (Marker) -> Unit = {}, + onInfoWindowLongClick: (Marker) -> Unit = {}, + pinConfig: PinConfig? = null, + iconView: View? = null, + collisionBehavior: Int = AdvancedMarkerOptions.CollisionBehavior.REQUIRED +) { + + AdvancedMarkerImpl( + state = state, + alpha = alpha, + anchor = anchor, + draggable = draggable, + flat = flat, + infoWindowAnchor = infoWindowAnchor, + rotation = rotation, + snippet = snippet, + tag = tag, + title = title, + visible = visible, + zIndex = zIndex, + onClick = onClick, + onInfoWindowClick = onInfoWindowClick, + onInfoWindowClose = onInfoWindowClose, + onInfoWindowLongClick = onInfoWindowLongClick, + pinConfig = pinConfig, + iconView = iconView, + collisionBehavior = collisionBehavior + ) +} + +/** + * Internal implementation for an advanced marker on a Google map. + * + * @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 + * @param flat sets if the marker should be flat against the map + * @param infoWindowAnchor the anchor point of the info window on the marker image + * @param rotation the rotation of the marker in degrees clockwise about the marker's anchor point + * @param snippet the snippet for the marker + * @param tag optional tag to associate with the marker + * @param title the title for the marker + * @param visible the visibility of the marker + * @param zIndex the z-index of the marker + * @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 + * @param onInfoWindowLongClick a lambda invoked when the marker's info window is long clicked + * @param infoWindow optional composable lambda expression for customizing + * the entire info window. If this value is non-null, the value in infoContent] + * will be ignored. + * @param infoContent optional composable lambda expression for customizing + * the info window's content. If this value is non-null, [infoWindow] must be null. + * @param pinConfig the PinConfig object that will be used for the advanced marker + * @param iconView the custom view to be used on the advanced marker + * @param collisionBehavior the expected collision behavior + */ +@Composable +@GoogleMapComposable +private fun AdvancedMarkerImpl( + + state: MarkerState = rememberMarkerState(), + alpha: Float = 1.0f, + anchor: Offset = Offset(0.5f, 1.0f), + draggable: Boolean = false, + flat: Boolean = false, + infoWindowAnchor: Offset = Offset(0.5f, 0.0f), + rotation: Float = 0.0f, + snippet: String? = null, + tag: Any? = null, + title: String? = null, + visible: Boolean = true, + zIndex: Float = 0.0f, + onClick: (Marker) -> Boolean = { false }, + onInfoWindowClick: (Marker) -> Unit = {}, + onInfoWindowClose: (Marker) -> Unit = {}, + onInfoWindowLongClick: (Marker) -> Unit = {}, + infoWindow: (@Composable (Marker) -> Unit)? = null, + infoContent: (@Composable (Marker) -> Unit)? = null, + pinConfig: PinConfig? = null, + iconView: View? = null, + collisionBehavior: Int = AdvancedMarkerOptions.CollisionBehavior.REQUIRED +) { + + val mapApplier = currentComposer.applier as? MapApplier + val compositionContext = rememberCompositionContext() + + val advancedMarkerOptions = AdvancedMarkerOptions() + .position(state.position) + .collisionBehavior(collisionBehavior) + if (iconView != null) { + advancedMarkerOptions.iconView(iconView) + } else if (pinConfig != null) { + advancedMarkerOptions.icon(BitmapDescriptorFactory.fromPinConfig(pinConfig)) + } + + ComposeNode( + factory = { + val marker = mapApplier?.map?.addMarker(advancedMarkerOptions) + ?: error("Error adding marker") + marker.tag = tag + MarkerNode( + compositionContext = compositionContext, + marker = marker, + markerState = state, + onMarkerClick = onClick, + onInfoWindowClick = onInfoWindowClick, + onInfoWindowClose = onInfoWindowClose, + onInfoWindowLongClick = onInfoWindowLongClick, + infoContent = infoContent, + infoWindow = infoWindow, + ) + }, + update = { + update(onClick) { this.onMarkerClick = it } + update(onInfoWindowClick) { this.onInfoWindowClick = it } + update(onInfoWindowClose) { this.onInfoWindowClose = it } + update(onInfoWindowLongClick) { this.onInfoWindowLongClick = it } + update(infoContent) { this.infoContent = it } + update(infoWindow) { this.infoWindow = it } + + set(alpha) { this.marker.alpha = it } + set(anchor) { this.marker.setAnchor(it.x, it.y) } + set(draggable) { this.marker.isDraggable = it } + set(flat) { this.marker.isFlat = it } + set(infoWindowAnchor) { this.marker.setInfoWindowAnchor(it.x, it.y) } + set(state.position) { this.marker.position = it } + set(rotation) { this.marker.rotation = it } + set(snippet) { + this.marker.snippet = it + if (this.marker.isInfoWindowShown) { + this.marker.showInfoWindow() + } + } + set(tag) { this.marker.tag = it } + set(title) { + this.marker.title = it + if (this.marker.isInfoWindowShown) { + this.marker.showInfoWindow() + } + } + set(pinConfig) { + if (iconView == null) { + this.marker.setIcon(pinConfig?.let { it1 -> + BitmapDescriptorFactory.fromPinConfig( + it1 + ) + }) + } + } + + set(visible) { this.marker.isVisible = it } + set(zIndex) { this.marker.zIndex = it } + } + ) +} 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 cc774f4c0..e73c7d53f 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