Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/lite-mode-dark-theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ GoogleMap(
However, when Lite Mode is enabled (`GoogleMapOptions().liteMode(true)`), the underlying Google Maps Android SDK (`play-services-maps`) functions differently:

1. **Static Pre-rendered Tiles**: Lite Mode maps fetch static raster tiles rather than rendering vectors on the client GPU.
2. **`MapColorScheme` Bypassed**: The Google Maps Android SDK explicitly ignores dynamic `MapColorScheme` configurations for Lite Mode maps.
2. **`MapColorScheme` Bypassed**: Lite Mode maps do not support `MapColorScheme`, so Maps Compose does not apply `mapColorScheme` to them. Older versions of Google Play services throw `UnsupportedOperationException` when a color scheme is set on a Lite Mode map.
3. **Map Types Remain in Light Mode**: Neither `MapType.NORMAL` nor `MapType.TERRAIN` will render dark tiles through `mapColorScheme`.

To support dark mode on a Lite Mode map, your application must listen for system theme changes and dynamically supply a custom dark JSON style using `MapProperties.mapStyleOptions`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class GoogleMapViewTests {
}

@Test
fun testLiteModePreservesColorSchemeAndLiteModeInOptions() {
fun testLiteModeDoesNotApplyColorSchemeToOptions() {
var capturedOptions: GoogleMapOptions? = null
composeTestRule.setContent {
GoogleMap(
Expand All @@ -183,7 +183,7 @@ class GoogleMapViewTests {
)
}
assertThat(capturedOptions?.liteMode).isTrue()
assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.DARK)
assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.LIGHT)
}

@Test
Expand All @@ -205,11 +205,11 @@ class GoogleMapViewTests {
}
assertThat(capturedOptions?.liteMode).isTrue()
assertThat(capturedOptions?.mapType).isEqualTo(GoogleMap.MAP_TYPE_TERRAIN)
assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.DARK)
assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.LIGHT)
}

@Test
fun testLiteModeDefaultColorSchemeIsFollowSystem() {
fun testLiteModeDoesNotApplyDefaultColorSchemeToOptions() {
var capturedOptions: GoogleMapOptions? = null
composeTestRule.setContent {
GoogleMap(
Expand All @@ -221,7 +221,27 @@ class GoogleMapViewTests {
)
}
assertThat(capturedOptions?.liteMode).isTrue()
assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.FOLLOW_SYSTEM)
assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.LIGHT)
}

@Test
fun testLiteModePreservesColorSchemeSetInOptions() {
var capturedOptions: GoogleMapOptions? = null
composeTestRule.setContent {
GoogleMap(
googleMapOptionsFactory = {
GoogleMapOptions()
.liteMode(true)
.mapColorScheme(MapColorScheme.DARK)
},
mapViewFactory = { context, options ->
capturedOptions = options
MapView(context, options)
}
)
}
assertThat(capturedOptions?.liteMode).isTrue()
assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.DARK)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import kotlinx.coroutines.launch
* @param contentPadding the padding values used to signal that portions of the map around the edges
* may be obscured. The map will move the Google logo, etc. to avoid overlapping the padding.
* @param mapColorScheme Defines the color scheme for the Map. Defaults to [ComposeMapColorScheme.FOLLOW_SYSTEM].
* Not applied to Lite mode maps, which do not support color schemes.
* @param content the content of the map
*/
@Composable
Expand Down Expand Up @@ -175,17 +176,21 @@ public fun GoogleMap(
// out of focus traversal entirely.
modifier = if (focusable) modifier.focusable() else modifier,
factory = { context ->
val options = googleMapOptionsFactory().let { opts ->
// If mapColorScheme is passed to GoogleMap() and has not been explicitly set
// in googleMapOptionsFactory (where 0 / MapColorScheme.LIGHT is the Java int default),
// apply it to GoogleMapOptions so MapView is created with it.
if (mapColorScheme != null && opts.mapColorScheme == 0) {
opts.mapColorScheme(mapColorScheme.value)
} else {
opts
}
val baseOptions = googleMapOptionsFactory()
val isLiteMode = baseOptions.liteMode == true
// If mapColorScheme is passed to GoogleMap() and has not been explicitly set
// in googleMapOptionsFactory (where 0 / MapColorScheme.LIGHT is the Java int default),
// apply it to GoogleMapOptions so MapView is created with it.
// Lite mode does not support color schemes, and older Play services throw
// UnsupportedOperationException for it, so it is never applied there (#1028).
val applyColorScheme =
mapColorScheme != null && baseOptions.mapColorScheme == 0 && !isLiteMode
val options = if (applyColorScheme) {
baseOptions.mapColorScheme(mapColorScheme.value)
} else {
baseOptions
}
cameraPositionState.isLiteMode = options.liteMode == true
cameraPositionState.isLiteMode = isLiteMode
mapViewFactory(context, options).also { mapView ->
mapView.applyFocusability(focusable)
mapView.setViewTreeLifecycleOwner(lifecycleOwner)
Expand All @@ -210,7 +215,11 @@ public fun GoogleMap(

val lifecycleObserver = MapLifecycleEventObserver(mapView)

mapView.tag = MapTagData(componentCallbacks, lifecycleObserver)
mapView.tag = MapTagData(
componentCallbacks,
lifecycleObserver,
isLiteMode = isLiteMode,
)

// Only register for [lifecycleOwner]'s lifecycle events while MapView is attached
val onAttachStateListener = object : View.OnAttachStateChangeListener {
Expand Down Expand Up @@ -290,7 +299,12 @@ private fun CoroutineScope.launchSubcomposition(
) {
val map = mapView.awaitMap()
val composition = Composition(
applier = MapApplier(map, mapView, mapClickListeners),
applier = MapApplier(
map,
mapView,
mapClickListeners,
isLiteMode = mapView.tagData.isLiteMode,
),
parent = parentComposition
)

Expand Down Expand Up @@ -336,7 +350,8 @@ internal class MapUpdaterState(
/** Used to store things in the tag which must be retrievable across recompositions */
private data class MapTagData(
val componentCallbacks: ComponentCallbacks,
val lifecycleObserver: MapLifecycleEventObserver
val lifecycleObserver: MapLifecycleEventObserver,
val isLiteMode: Boolean,
)

private val MapView.tagData: MapTagData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ internal class MapApplier(
val map: GoogleMap,
internal val mapView: MapView,
val mapClickListeners: MapClickListeners,
/** Whether [mapView] was created in Lite mode, which does not support color schemes. */
internal val isLiteMode: Boolean,
) : AbstractApplier<MapNode>(MapNodeRoot) {

private val decorations = mutableListOf<MapNode>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public val DefaultMapContentPadding: PaddingValues = PaddingValues()
internal inline fun MapUpdater(mapUpdaterState: MapUpdaterState) = with(mapUpdaterState) {
val map = (currentComposer.applier as MapApplier).map
val mapView = (currentComposer.applier as MapApplier).mapView
val isLiteMode = (currentComposer.applier as MapApplier).isLiteMode
if (mergeDescendants) {
mapView.importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
}
Expand Down Expand Up @@ -138,7 +139,8 @@ internal inline fun MapUpdater(mapUpdaterState: MapUpdaterState) = with(mapUpdat
set(mapProperties.maxZoomPreference) { map.setMaxZoomPreference(it) }
set(mapProperties.minZoomPreference) { map.setMinZoomPreference(it) }
set(mapColorScheme) {
if (it != null) {
// Lite mode does not support color schemes; see the GoogleMapOptions setup in GoogleMap.
if (it != null && !isLiteMode) {
map.mapColorScheme = it
}
}
Expand Down
Loading