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.
+ }
}
)
}