diff --git a/android/src/main/java/com/luggmaps/LuggMapView.kt b/android/src/main/java/com/luggmaps/LuggMapView.kt index 3a72dc3..98a735f 100644 --- a/android/src/main/java/com/luggmaps/LuggMapView.kt +++ b/android/src/main/java/com/luggmaps/LuggMapView.kt @@ -1,7 +1,6 @@ package com.luggmaps import android.annotation.SuppressLint -import android.content.res.Configuration import android.view.View import android.view.View.VISIBLE import com.facebook.react.uimanager.ThemedReactContext @@ -166,15 +165,7 @@ class LuggMapView(private val reactContext: ThemedReactContext) : // A user-provided background acts as the placeholder if (background != null) return - val isDark = when (theme) { - "dark" -> true - - "light" -> false - - else -> - (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == - Configuration.UI_MODE_NIGHT_YES - } + val isDark = GoogleMapProvider.isDarkTheme(theme, context.resources.configuration.uiMode) mapWrapperView?.setBackgroundColor(if (isDark) COLOR_PLACEHOLDER_DARK else COLOR_PLACEHOLDER_LIGHT) } diff --git a/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt b/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt index 86d0fd6..fb332c1 100644 --- a/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +++ b/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt @@ -1,6 +1,7 @@ package com.luggmaps import android.annotation.SuppressLint +import android.content.res.Configuration import android.view.MotionEvent import android.view.View import com.facebook.react.uimanager.ThemedReactContext @@ -21,8 +22,23 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context) // provider can create a map sized to the view var onLayoutReady: (() -> Unit)? = null + // View configs include in-app night mode overrides (AppCompat), unlike + // app-wide ComponentCallbacks. Also invoked on attach, since detached + // views miss changes + var onConfigurationChange: ((Configuration) -> Unit)? = null + private val measureAndLayoutChild = Runnable { layoutChild(width, height) } + override fun onAttachedToWindow() { + super.onAttachedToWindow() + onConfigurationChange?.invoke(resources.configuration) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + onConfigurationChange?.invoke(newConfig) + } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { touchEventHandler?.invoke(event) return super.dispatchTouchEvent(event) diff --git a/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt b/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt index 4109b6b..70e0bfb 100644 --- a/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +++ b/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt @@ -1,7 +1,6 @@ package com.luggmaps.core import android.annotation.SuppressLint -import android.content.ComponentCallbacks2 import android.content.Context import android.content.res.Configuration import android.os.SystemClock @@ -27,6 +26,7 @@ import com.google.android.gms.maps.model.GroundOverlayOptions import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLngBounds import com.google.android.gms.maps.model.MapColorScheme +import com.google.android.gms.maps.model.MapStyleOptions import com.google.android.gms.maps.model.Marker import com.google.android.gms.maps.model.MarkerOptions import com.google.android.gms.maps.model.Polygon @@ -76,8 +76,7 @@ class GoogleMapProvider(private val context: Context) : GoogleMap.OnGroundOverlayClickListener, GoogleMap.OnMarkerClickListener, GoogleMap.OnMarkerDragListener, - GoogleMap.InfoWindowAdapter, - ComponentCallbacks2 { + GoogleMap.InfoWindowAdapter { override var delegate: MapProviderDelegate? = null override val isMapReady: Boolean get() = _isMapReady @@ -107,6 +106,7 @@ class GoogleMapProvider(private val context: Context) : private val markerToViewMap = mutableMapOf() private val liveMarkerViews = mutableSetOf() private var staticProjectionReady = false + private var isLiteMode = false private var activeNonBubbledMarker: Marker? = null private var tapLocation: LatLng? = null @@ -162,7 +162,7 @@ class GoogleMapProvider(private val context: Context) : wrapper.relayoutChildOnRequest = staticMode this.wrapperView = wrapper - context.applicationContext.registerComponentCallbacks(this) + wrapper.onConfigurationChange = ::updateNightMode // Static mode needs the view size to pick lite vs full map, so wait // for the first layout if it hasn't happened yet @@ -180,11 +180,11 @@ class GoogleMapProvider(private val context: Context) : // Lite mode renders the map as a single bitmap capped at ~2048px per // dimension, centered in the view β€” bigger views get letterboxed. Fall // back to a full map (gestures disabled) when the view exceeds the cap - val useLite = staticMode && maxOf(wrapper.width, wrapper.height) <= LITE_MODE_MAX_SIZE - val options = GoogleMapOptions().liteMode(useLite) + isLiteMode = staticMode && maxOf(wrapper.width, wrapper.height) <= LITE_MODE_MAX_SIZE + val options = GoogleMapOptions().liteMode(isLiteMode) // Lite mode doesn't support map IDs (cloud-based styling); setting one // makes the static map render blank - if (!useLite) options.mapId(mapId) + if (!isLiteMode) options.mapId(mapId) mapView = MapView(context, options).also { view -> view.onCreate(null) view.onResume() @@ -196,9 +196,9 @@ class GoogleMapProvider(private val context: Context) : override fun destroy() { wrapperView?.onLayoutReady = null + wrapperView?.onConfigurationChange = null detachWindowInsetsListener() detachWatermarkLayoutListener() - context.applicationContext.unregisterComponentCallbacks(this) dismissNonBubbledCallout() for (markerView in liveMarkerViews) { markerView.onUpdate = null @@ -1588,20 +1588,21 @@ class GoogleMapProvider(private val context: Context) : mapView?.let { ViewCompat.setOnApplyWindowInsetsListener(it, null) } } - override fun onConfigurationChanged(newConfig: Configuration) { - val newNightMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK + private fun updateNightMode(config: Configuration) { + val newNightMode = config.uiMode and Configuration.UI_MODE_NIGHT_MASK if (newNightMode != currentNightMode) { currentNightMode = newNightMode applyTheme() } } - @Deprecated("Deprecated in Java") - override fun onLowMemory() {} - - override fun onTrimMemory(level: Int) {} - private fun applyTheme() { + // Lite mode ignores mapColorScheme, but supports JSON styling + if (isLiteMode) { + val style = if (isDarkTheme(theme, currentNightMode)) MapStyleOptions(LITE_MODE_DARK_STYLE) else null + googleMap?.setMapStyle(style) + return + } val colorScheme = when (theme) { "dark" -> MapColorScheme.DARK "light" -> MapColorScheme.LIGHT @@ -1632,5 +1633,25 @@ class GoogleMapProvider(private val context: Context) : // Undocumented cap on the lite mode bitmap (2048 minus a 2px border, // measured empirically) private const val LITE_MODE_MAX_SIZE = 2046 + + // Approximates MapColorScheme.DARK so lite and full maps match + private const val LITE_MODE_DARK_STYLE = """[ + {"elementType":"geometry","stylers":[{"color":"#1c2a40"}]}, + {"elementType":"labels.text.fill","stylers":[{"color":"#b0b0b0"}]}, + {"elementType":"labels.text.stroke","stylers":[{"color":"#1c2a40"}]}, + {"featureType":"poi.park","elementType":"geometry","stylers":[{"color":"#144a57"}]}, + {"featureType":"road","elementType":"geometry.fill","stylers":[{"color":"#3c4d62"}]}, + {"featureType":"road","elementType":"geometry.stroke","stylers":[{"color":"#1c2a40"}]}, + {"featureType":"road.highway","elementType":"geometry.fill","stylers":[{"color":"#3e5a77"}]}, + {"featureType":"transit","elementType":"geometry","stylers":[{"color":"#23314f"}]}, + {"featureType":"water","elementType":"geometry","stylers":[{"color":"#00102e"}]} + ]""" + + fun isDarkTheme(theme: String, uiMode: Int): Boolean = + when (theme) { + "dark" -> true + "light" -> false + else -> (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + } } } diff --git a/docs/content/docs/components/map-view.mdx b/docs/content/docs/components/map-view.mdx index 9be7557..2dd746b 100644 --- a/docs/content/docs/components/map-view.mdx +++ b/docs/content/docs/components/map-view.mdx @@ -112,6 +112,9 @@ Notes: appears when ready, and marker prop updates still apply. - While a static map loads, a theme-aware placeholder background is shown. Set a `backgroundColor` style on the `MapView` to use your own. +- On Android, lite mode doesn't support Google's dark color scheme, so a + dark `theme` (or `'system'` in dark mode) applies a built-in dark style + instead. - Set `staticKey` (e.g. your list item's id) to cache base map images across list recycling on iOS - scrolling back to a row reuses its image instead of rendering the map again (markers and shapes stay live and diff --git a/skills/maps-usage/references/configuration.md b/skills/maps-usage/references/configuration.md index 6513cc4..7c9c8ef 100644 --- a/skills/maps-usage/references/configuration.md +++ b/skills/maps-usage/references/configuration.md @@ -150,7 +150,7 @@ export function App() { | Prop | Type | Default | Platforms | Description | |------|------|---------|-----------|-------------| -| `staticMode` | `boolean` | `false` | πŸŽπŸ…ΆπŸ€–πŸŒ | Creation-time. Non-interactive map for lists. Android lite mode (bitmap ≀ ~2048px, larger views fall back to a gesture-less full map), iOS snapshot, web live map with gestures/POI clicks/keyboard/press off | +| `staticMode` | `boolean` | `false` | πŸŽπŸ…ΆπŸ€–πŸŒ | Creation-time. Non-interactive map for lists. Android lite mode (bitmap ≀ ~2048px, larger views fall back to a gesture-less full map; dark `theme` uses a built-in style), iOS snapshot, web live map with gestures/POI clicks/keyboard/press off | | `staticKey` | `string` | β€” | πŸŽπŸ…Ά | Stable id for the base-image cache (e.g. row id). Camera, size, provider, and map settings are part of the key automatically. Changing it discards the current image. Only fully loaded renders are cached | Static-mode behavior notes: diff --git a/skills/maps-usage/references/troubleshooting.md b/skills/maps-usage/references/troubleshooting.md index 59e6a64..f5f2783 100644 --- a/skills/maps-usage/references/troubleshooting.md +++ b/skills/maps-usage/references/troubleshooting.md @@ -170,6 +170,8 @@ Common issues with `@lugg/maps`, organized by symptom. **Fix:** Accept the default style for lite maps, or render a small live map with all gestures disabled (`zoomEnabled={false} scrollEnabled={false} rotateEnabled={false} pitchEnabled={false}`) when styling matters more than memory. +`theme` still works on lite maps: dark uses a built-in JSON style approximating Google's dark scheme (lite mode ignores the color scheme). + ## GeoJSON renders in the wrong place **Cause:** Coordinates were authored as `[lat, lng]`.