diff --git a/docs/lite-mode-dark-theme.md b/docs/lite-mode-dark-theme.md index dd664a6aa..5b13a09d3 100644 --- a/docs/lite-mode-dark-theme.md +++ b/docs/lite-mode-dark-theme.md @@ -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`. diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt index 9ef1dde58..0dc6d11ae 100644 --- a/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt @@ -170,7 +170,7 @@ class GoogleMapViewTests { } @Test - fun testLiteModePreservesColorSchemeAndLiteModeInOptions() { + fun testLiteModeDoesNotApplyColorSchemeToOptions() { var capturedOptions: GoogleMapOptions? = null composeTestRule.setContent { GoogleMap( @@ -183,7 +183,7 @@ class GoogleMapViewTests { ) } assertThat(capturedOptions?.liteMode).isTrue() - assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.DARK) + assertThat(capturedOptions?.mapColorScheme).isEqualTo(MapColorScheme.LIGHT) } @Test @@ -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( @@ -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 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 6edd9d4e7..98b91206f 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 @@ -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 @@ -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) @@ -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 { @@ -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 ) @@ -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 diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt index f1793069f..65132abeb 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapApplier.kt @@ -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(MapNodeRoot) { private val decorations = mutableListOf() diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt index fb44dae86..d27aa9a36 100644 --- a/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt +++ b/maps-compose/src/main/java/com/google/maps/android/compose/MapUpdater.kt @@ -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 } @@ -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 } }