Skip to content
Merged
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
11 changes: 1 addition & 10 deletions android/src/main/java/com/luggmaps/LuggMapView.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
16 changes: 16 additions & 0 deletions android/src/main/java/com/luggmaps/LuggMapWrapperView.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down
51 changes: 36 additions & 15 deletions android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -107,6 +106,7 @@ class GoogleMapProvider(private val context: Context) :
private val markerToViewMap = mutableMapOf<Marker, LuggMarkerView>()
private val liveMarkerViews = mutableSetOf<LuggMarkerView>()
private var staticProjectionReady = false
private var isLiteMode = false
private var activeNonBubbledMarker: Marker? = null
private var tapLocation: LatLng? = null

Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}
3 changes: 3 additions & 0 deletions docs/content/docs/components/map-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion skills/maps-usage/references/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions skills/maps-usage/references/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`.
Expand Down
Loading