Skip to content

feat(mapview): add staticMode prop for static maps in lists#85

Merged
lodev09 merged 17 commits into
mainfrom
feat/static-mode
Jul 7, 2026
Merged

feat(mapview): add staticMode prop for static maps in lists#85
lodev09 merged 17 commits into
mainfrom
feat/static-mode

Conversation

@lodev09

@lodev09 lodev09 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a staticMode prop to MapView for rendering non-interactive static maps, optimized for list views where mounting many live maps is expensive, plus a staticKey prop for snapshot caching across list recycling.

  • Android (Google) - uses lite mode (GoogleMapOptions.liteMode), rendering a static bitmap instead of a live GL surface. mapId is skipped (unsupported in lite mode), the lite toolbar is disabled, and live (non-rasterized) markers stay hidden until the map has loaded so they never flash at an invalid projection.
  • iOS - static maps render fully async through a shared StaticMapProviderBase: markers are live overlay views positioned via a mercator projection (async content like remote images still updates), polylines/polygons/circles/ground overlays draw into a single CoreGraphics overlay, and all overlays stay hidden over a theme-aware placeholder until the base map displays.
    • Apple - base map renders with MKMapSnapshotter, entirely off the main thread; no live map view is ever created.
    • Google - the SDK has no async snapshotter, so the base map warms up briefly on a live, tiles-only GMSMapView under the overlays, then swaps to a rendered image (swap deferred outside scroll tracking). Warmup map views are pooled and reused; overlays reveal as soon as tiles finish rendering. Warmup concurrency is unbounded - apps control it via list props (windowSize, etc.).
    • staticKey caches base-only snapshot images (bounded NSCache) keyed by camera, size, provider, and map settings; markers/shapes always re-render live from props.
    • animated polylines render as complete static polylines.
  • Web - forces gestureHandling: 'none'.

staticMode is creation-time only. On iOS, camera commands and prop updates are no-ops after the base map renders.

Example: new StaticMapsScreen (shared) renders a 100-row FlatList of pressable place cards cycling marker use-cases (default, icon, text, remote image, multiple) plus a per-card constellation polyline with dot markers, with an Apple/Google provider toggle. Wired into bare (StaticMaps route) and expo (/static-maps).

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Test Plan

  • yarn typecheck, yarn test, yarn lint, ktlint, objclint pass
  • Example Static Maps screen (100 rows): iOS Apple and Google providers fill in while scrolling without blocking the UI thread; Android renders lite mode with markers appearing only once positioned; row press navigates while maps ignore touches
  • Scrolled back through rendered rows to verify staticKey cache reuse

Screenshots / Videos

N/A

Checklist

  • I tested on iOS
  • I tested on Android
  • I tested on Web
  • I updated the documentation (if needed)

lodev09 added 17 commits July 6, 2026 22:47
drawMapRect: runs on MapKit's background render queues; snapshot
polyline/distances/colors so main-thread updates can't cause
out-of-bounds reads or use-after-free of the color cache
- reset static content on staticKey change (prevents stale/poisoned
  snapshot cache with in-place list recycling)
- re-arm snapshot swap on zero-size capture instead of giving up
- bound snapshot cache by memory; include camera in cache key
- reset recycled wrapper placeholder styling
- web: disable POI clicks, keyboard, and press handlers in staticMode
No live MKMapView is created - the base map renders off the main thread
and keeps loading while scrolling, like Android lite mode. Markers are
live views positioned via mercator projection; shapes are drawn onto a
CoreGraphics overlay. Removes the old snapshot-swap path from
AppleMapProvider (now Google-only via the warmup queue).
Creating and tearing down a GMSMapView are expensive main-thread
operations; detached static maps are pooled (capacity 3, keyed by map
ID) and reused, so a warmup usually only costs a camera move plus tile
load. Pool drains on memory warnings.
…ase warmup

Extract Apple static provider's overlay machinery (projection, live
marker views, CG shape overlay) into StaticMapProviderBase and add
GoogleStaticMapProvider: markers/shapes show immediately without a
warmup slot; only the base map warms up on a pooled, tiles-only
GMSMapView below the overlays, then swaps to an image. Strips static
mode from GoogleMapProvider and warmup bookkeeping from LuggMapView.
Live (non-rasterized) markers were positioned with a garbage projection
before the lite map rendered, flashing off-position until onMapLoaded
corrected them. Keep them invisible until positioned correctly.
Markers and shapes no longer show over the placeholder while the base
map loads; they stay hidden and appear together with it, matching
Android lite mode.
Google static maps now warm up as soon as rows mount instead of queuing
behind a 3-slot, scroll-gated warmup queue. Apps bound concurrent work
via list props (windowSize etc.).
The swap is deferred while scrolling, so markers stayed hidden over an
already-rendered warmup map. Decouple the overlay reveal from the swap.
@lodev09 lodev09 merged commit 1aa1f75 into main Jul 7, 2026
3 checks passed
@lodev09 lodev09 deleted the feat/static-mode branch July 7, 2026 20:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant