Skip to content

fix(map): replace MarkerComposable with Canvas-rendered bitmaps#5702

Merged
jamesarich merged 3 commits into
mainfrom
jamesarich/fix-inline-map-marker-zero-size-crash
Jun 1, 2026
Merged

fix(map): replace MarkerComposable with Canvas-rendered bitmaps#5702
jamesarich merged 3 commits into
mainfrom
jamesarich/fix-inline-map-marker-zero-size-crash

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

Why

The maps-compose library's MarkerComposable and rememberComposeBitmapDescriptor create an off-screen ComposeView, measure it synchronously, and throw if width/height is zero. This causes a recurring crash (renderComposableToBitmapDescriptor -- "measured to have a width or height of zero") that has been hitting production since 2.7.3 and is still present in build 29321001 (Crashlytics issue 66ee057e).

The root cause is a subcomposition race: when the Recomposer is paused or the parent hasn't laid out yet, the synchronous measure returns zero and the library throws unconditionally. Unlike the Clustering composable (which has internal fakeLifecycleOwner protection), standalone MarkerComposable has no safety net.

Approach

Replace all standalone MarkerComposable/rememberComposeBitmapDescriptor usages across the app with Canvas-rendered bitmap descriptors that never go through a ComposeView:

  • New utility: MarkerBitmapRenderer.kt provides rememberNodeChipDescriptor() and rememberEmojiMarkerDescriptor() -- pure Canvas/TextPaint rendering with proper remember caching.
  • InlineMap.kt: Replaced the crash-site MarkerComposable { NodeChip(...) } with Marker(icon = rememberNodeChipDescriptor(node)).
  • MapView.kt: Replaced both NodeTrackOverlay and TracerouteMapContent marker composables.
  • WaypointMarkers.kt: Replaced emoji rememberComposeBitmapDescriptor with rememberEmojiMarkerDescriptor.

The Clustering composable in NodeClusterMarkers.kt is intentionally untouched -- it uses the library's internal ComposeUiClusterRenderer which has its own lifecycle protection.

Visual fidelity

The Canvas renderer matches the original NodeChip composable:

  • Same dimensions (64dp min-width, 28dp min-height, 8dp horizontal padding)
  • Same styling (4dp corner radius, 14sp bold text, theme colors)
  • Strikethrough text when node.isIgnored
  • Respects user font scale (accessibility) via scaledDensity

Notes for reviewers

  • All click handlers, z-indexing, and alpha values are preserved on the replacement Marker composables.
  • The @OptIn(MapsComposeExperimentalApi::class) remains on NodeTrackOverlay for MarkerInfoWindowComposable (used for breadcrumb markers, not node chips).
  • No new dependencies added.

jamesarich and others added 2 commits June 1, 2026 09:49
Eliminates the FATAL crash "The ComposeView was measured to have a
width or height of zero" (Crashlytics issue 66ee057e) caused by
maps-compose's renderComposableToBitmapDescriptor creating off-screen
ComposeViews that can fail to measure during subcomposition races.

Instead of relying on the library's fragile synchronous compose→measure
pipeline, render marker bitmaps directly using Canvas:
- NodeChip markers: colored rounded rect + centered text
- Waypoint emoji markers: text painted at 32sp

This removes all usages of MarkerComposable and
rememberComposeBitmapDescriptor across InlineMap, MapView (NodeTrack
and Traceroute overlays), and WaypointMarkers.

Fixes: googlemaps/android-maps-compose#875 (zero-size variant)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review fixes to the Canvas-rendered marker bitmaps:
- Render strikethrough text when node.isIgnored (matching NodeChip)
- Include isIgnored in remember keys so bitmap recomputes on toggle
- Use scaledDensity (density * fontScale) for text sizing to respect
  accessibility font scale settings

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the bugfix PR tag label Jun 1, 2026
@jamesarich jamesarich marked this pull request as ready for review June 1, 2026 17:03
@jamesarich jamesarich enabled auto-merge June 1, 2026 17:03
@jamesarich jamesarich disabled auto-merge June 1, 2026 17:09
@jamesarich jamesarich enabled auto-merge June 1, 2026 17:09
@jamesarich jamesarich disabled auto-merge June 1, 2026 17:29
@jamesarich jamesarich merged commit 60feec6 into main Jun 1, 2026
17 checks passed
@jamesarich jamesarich deleted the jamesarich/fix-inline-map-marker-zero-size-crash branch June 1, 2026 17:29
jamesarich added a commit that referenced this pull request Jun 2, 2026
…p, drop Google Maps leftovers

Rebased onto main and dusted off the MapLibre Compose Multiplatform branch.

- Bump maplibre-compose 0.12.1 -> 0.13.0 and migrate the redesigned location API
  (BearingUpdate.TRACK_LOCATION -> TRACK_AUTOMATIC; LocationPuck locationState -> location).
- desktopApp: add a host-detecting maplibre-native-bindings-jni runtime backend so the
  desktop base map renders (Metal on macOS arm64, OpenGL on Linux/Windows amd64). Without it
  Gradle links no native renderer and the map canvas is black.
- Gate Compose map overlays off on the JVM target via a mapOverlaysSupported expect/actual flag
  (true Android/iOS, false desktop). maplibre-compose 0.13.0 stubs the desktop layers/sources
  API with TODO(), so composing markers/waypoints/tracks/traceroute threw NotImplementedError
  and tore down the window. Desktop now renders base-map-only; overlays auto-enable when upstream
  implements desktop layers.
- Remove orphaned Google Maps leftovers the rebase carried over from main's #5702/#5709 marker
  work (MarkerBitmapRenderer.kt + play-services-maps); this branch replaces Google Maps entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant