diff --git a/maps-app/src/main/AndroidManifest.xml b/maps-app/src/main/AndroidManifest.xml index e7af6d8e..4b4acc3e 100644 --- a/maps-app/src/main/AndroidManifest.xml +++ b/maps-app/src/main/AndroidManifest.xml @@ -103,6 +103,9 @@ + diff --git a/maps-app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt index 43b74a19..14905244 100644 --- a/maps-app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt +++ b/maps-app/src/main/java/com/google/maps/android/compose/BasicMapActivity.kt @@ -38,12 +38,12 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape -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.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -123,7 +123,7 @@ class BasicMapActivity : ComponentActivity() { ) { CircularProgressIndicator( modifier = Modifier - .background(MaterialTheme.colors.background) + .background(MaterialTheme.colorScheme.background) .wrapContentSize() ) } @@ -255,8 +255,8 @@ fun GoogleMapView( Circle( center = circleCenter, - fillColor = MaterialTheme.colors.secondary, - strokeColor = MaterialTheme.colors.secondaryVariant, + fillColor = MaterialTheme.colorScheme.secondary, + strokeColor = MaterialTheme.colorScheme.secondary, radius = 1000.0, ) @@ -400,12 +400,12 @@ private fun MapButton(text: String, onClick: () -> Unit, modifier: Modifier = Mo Button( modifier = modifier.padding(4.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.onPrimary, - contentColor = MaterialTheme.colors.primary + containerColor = MaterialTheme.colorScheme.onPrimary, + contentColor = MaterialTheme.colorScheme.primary ), onClick = onClick ) { - Text(text = text, style = MaterialTheme.typography.body1) + Text(text = text, style = MaterialTheme.typography.bodyLarge) } } diff --git a/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt b/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt index 5fd362c3..410785c7 100644 --- a/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt +++ b/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt @@ -111,6 +111,11 @@ sealed class ActivityGroup( R.string.tile_overlay_activity_description, TileOverlayActivity::class ), + Activity( + R.string.ground_overlay_activity, + R.string.ground_overlay_activity_description, + GroundOverlayActivity::class + ), ) ) diff --git a/maps-app/src/main/java/com/google/maps/android/compose/GroundOverlayActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/GroundOverlayActivity.kt new file mode 100644 index 00000000..ca94a4b6 --- /dev/null +++ b/maps-app/src/main/java/com/google/maps/android/compose/GroundOverlayActivity.kt @@ -0,0 +1,173 @@ +// Copyright 2026 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 java.util.Locale +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import com.google.maps.android.compose.theme.MapsComposeSampleTheme +import androidx.core.graphics.createBitmap + +class GroundOverlayActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MapsComposeSampleTheme { + GroundOverlayScreen() + } + } + } +} + +@Composable +fun GroundOverlayScreen() { + val singapore = LatLng(1.3588227, 103.8742114) + val cameraPositionState = rememberCameraPositionState { + position = com.google.android.gms.maps.model.CameraPosition.fromLatLngZoom(singapore, 12f) + } + + var isVisible by remember { mutableStateOf(true) } + var transparency by remember { mutableFloatStateOf(0f) } + var bearing by remember { mutableFloatStateOf(0f) } + + val context = androidx.compose.ui.platform.LocalContext.current + val imageDescriptor = remember { + val drawable = androidx.core.content.ContextCompat.getDrawable(context, R.mipmap.ic_launcher) + val bitmap = createBitmap(drawable!!.intrinsicWidth, drawable.intrinsicHeight) + val canvas = android.graphics.Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + BitmapDescriptorFactory.fromBitmap(bitmap) + } + + Box( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + ) { + GoogleMap( + modifier = Modifier.fillMaxSize(), + cameraPositionState = cameraPositionState + ) { + // Stable GroundOverlay (using remembered state) + GroundOverlay( + position = GroundOverlayPosition.create( + LatLngBounds( + LatLng(1.35, 103.86), + LatLng(1.37, 103.88) + ) + ), + image = imageDescriptor, + visible = isVisible, + transparency = transparency, + bearing = bearing + ) + + // Stress-test GroundOverlay: Re-creating position every recomposition + // This would cause a crash/rendering loop if GroundOverlayPosition was not a data class + GroundOverlay( + position = GroundOverlayPosition.create( + LatLngBounds( + LatLng(1.36, 103.89), + LatLng(1.38, 103.91) + ) + ), + image = imageDescriptor, + transparency = 0.5f, + zIndex = 1f + ) + } + + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp) + ) { + GroundOverlayControls( + isVisible = isVisible, + onVisibilityChange = { isVisible = it }, + transparency = transparency, + onTransparencyChange = { transparency = it }, + bearing = bearing, + onBearingChange = { bearing = it } + ) + } + } +} + +@Composable +fun GroundOverlayControls( + isVisible: Boolean, + onVisibilityChange: (Boolean) -> Unit, + transparency: Float, + onTransparencyChange: (Float) -> Unit, + bearing: Float, + onBearingChange: (Float) -> Unit +) { + Surface( + shape = MaterialTheme.shapes.medium, + tonalElevation = 4.dp, + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = "Visible", modifier = Modifier.weight(1f)) + Switch(checked = isVisible, onCheckedChange = onVisibilityChange) + } + Text(text = "Transparency: ${String.format(Locale.getDefault(), "%.2f", transparency)}") + Slider( + value = transparency, + onValueChange = onTransparencyChange, + valueRange = 0f..1f + ) + Text(text = "Bearing: ${bearing.toInt()}°") + Slider( + value = bearing, + onValueChange = onBearingChange, + valueRange = 0f..360f + ) + } + } +} diff --git a/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt index 4deded30..86ba4a61 100644 --- a/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt +++ b/maps-app/src/main/java/com/google/maps/android/compose/markerexamples/MarkerClusteringActivity.kt @@ -2,6 +2,7 @@ package com.google.maps.android.compose.markerexamples import android.os.Bundle import android.util.Log +import java.util.Locale import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -17,11 +18,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect @@ -177,7 +178,7 @@ private fun CustomUiClustering(items: List) { clusterContent = { cluster -> CircleContent( modifier = Modifier.size(40.dp), - text = "%,d".format(cluster.size), + text = "%,d".format(Locale.getDefault(), cluster.size), color = Color.Blue, ) }, @@ -210,7 +211,7 @@ fun CustomRendererClustering(items: List) { clusterContent = { cluster -> CircleContent( modifier = Modifier.size(40.dp), - text = "%,d".format(cluster.size), + text = "%,d".format(Locale.getDefault(), cluster.size), color = Color.Green, ) }, @@ -306,12 +307,12 @@ private fun MapButton(text: String, onClick: () -> Unit, modifier: Modifier = Mo Button( modifier = modifier.padding(4.dp), colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.onPrimary, - contentColor = MaterialTheme.colors.primary + containerColor = MaterialTheme.colorScheme.onPrimary, + contentColor = MaterialTheme.colorScheme.primary ), onClick = onClick ) { - Text(text = text, style = MaterialTheme.typography.body1) + Text(text = text, style = MaterialTheme.typography.bodyLarge) } } diff --git a/maps-app/src/main/java/com/google/maps/android/compose/theme/Theme.kt b/maps-app/src/main/java/com/google/maps/android/compose/theme/Theme.kt index faa7548a..d1274f5d 100644 --- a/maps-app/src/main/java/com/google/maps/android/compose/theme/Theme.kt +++ b/maps-app/src/main/java/com/google/maps/android/compose/theme/Theme.kt @@ -1,9 +1,9 @@ package com.google.maps.android.compose.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable @Composable @@ -12,7 +12,7 @@ fun MapsComposeSampleTheme( content: @Composable () -> Unit ) { MaterialTheme( - colors = if (darkTheme) darkColors() else lightColors(), + colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme(), content = content ) } diff --git a/maps-app/src/main/res/values/strings.xml b/maps-app/src/main/res/values/strings.xml index de8d3772..9eb2b89e 100644 --- a/maps-app/src/main/res/values/strings.xml +++ b/maps-app/src/main/res/values/strings.xml @@ -72,6 +72,9 @@ Tile Overlay Adding a tile overlay to the map. + Ground Overlay + Adding a ground overlay to the map. + Map Types Map Features diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt index 3f753cb4..2f3e5ece 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/GroundOverlay.kt @@ -40,7 +40,8 @@ internal class GroundOverlayNode( * * Use one of the [create] methods to construct an instance of this class. */ -public class GroundOverlayPosition private constructor( +@ConsistentCopyVisibility +public data class GroundOverlayPosition internal constructor( public val latLngBounds: LatLngBounds? = null, public val location: LatLng? = null, public val width: Float? = null, @@ -116,6 +117,11 @@ public fun GroundOverlay( update(transparency) { this.groundOverlay.transparency = it } update(visible) { this.groundOverlay.isVisible = it } update(zIndex) { this.groundOverlay.zIndex = it } + update(anchor) { + // GroundOverlay does not have a setAnchor method. + // We could recreate the overlay here, but that might be expensive. + // For now, we'll document that anchor cannot be changed. + } } ) }