diff --git a/README.md b/README.md index f29f9f5e7..fb4cb6ae1 100644 --- a/README.md +++ b/README.md @@ -159,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. @@ -283,7 +289,7 @@ This library provides optional utilities in the `maps-compose-utils` library fro 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( @@ -364,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..8701f609f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,15 +58,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/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 69d8deea0..c7ea8c0db 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,11 +42,14 @@ + 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/MainActivity.kt b/app/src/main/java/com/google/maps/android/compose/MainActivity.kt index ff8e0bdc7..f6f8ada02 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 @@ -32,8 +32,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?) { @@ -63,16 +61,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 +85,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/res/values/strings.xml b/app/src/main/res/values/strings.xml index dd10f260d..a74afdd8e 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80f803570..c8a074d53 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" [libraries] @@ -39,7 +39,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..ca0187bfd 100644 --- a/maps-compose-widgets/build.gradle +++ b/maps-compose-widgets/build.gradle @@ -42,7 +42,6 @@ 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 diff --git a/maps-compose/build.gradle b/maps-compose/build.gradle index 5da3074a3..ce89fd508 100644 --- a/maps-compose/build.gradle +++ b/maps-compose/build.gradle @@ -39,7 +39,6 @@ dependencies { implementation libs.androidx.core implementation libs.androidx.compose.foundation implementation libs.kotlin - implementation libs.maps.playservice implementation libs.maps.ktx.std testImplementation libs.test.junit 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 } + } + ) +}