Migrate Android MapLibre Compose UI to the official SDK #853
Migrate Android MapLibre Compose UI to the official SDK #853klemensz wants to merge 18 commits intostadiamaps:mainfrom
Conversation
Migrates the Android phone and tablet navigation views from the legacy dependency to the official `org.maplibre.compose:maplibre-compose-android` artifact. Key changes include: * Introduced `NavigationMapState` to manage camera modes (follow user, overview, free) and zoom behavior. * Replaced `MapViewCamera` with a custom camera layer supporting automatic orientation and recentering logic. * Updated route rendering to use GeoJSON sources and `LineLayer` instead of legacy polyline APIs. * Added `NavigationMapPuckStyle` for configurable location puck appearance. * Refactored `NavigationMapView` to support official `MapOptions`, `LocationPuck`, and native `Style` access. * Updated gesture handling to use Ferrostar-specific `NavigationMapClickHandler` returning geographic coordinates. * Maintained legacy compatibility for Android Auto in `ui-maplibre-car-app` by keeping the Rallista dependency for that module. * Added unit tests for GeoJSON serialization, location mapping, and camera state logic.
…e` and refine demo UI
…ate logic to differentiate between "template" and "tracking" camera positions.
…d introduce a specialized navigation puck overlay.
…igationMapView` The `NavigationMapView` component in the Android MapLibre UI module now exposes `onMapLoadFinished` and `onMapLoadFailed` callbacks instead of the legacy `onMapReadyCallback`. This change removes the internal reflection-based workaround (`nativeStyleOrNull`) previously used to extract the native MapLibre `Style` from the compose `CameraState`.
… Android Auto UI
| LocationTrackingEffect( | ||
| locationState = userLocationState, | ||
| enabled = navigationMapState.isTrackingUser, | ||
| trackBearing = navigationMapState.cameraMode == NavigationCameraMode.FOLLOW_USER_WITH_BEARING, | ||
| ) { | ||
| cameraState.position = | ||
| navigationMapState.trackingFollowingCameraPosition( | ||
| target = currentLocation.position, | ||
| bearing = currentLocation.bearing, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Well... 😅 I guess this is why the video looked like a clock going tick tock advancing every second haha.
I assume this is a limitation inherent in the current MapLibre Compose API?
- Do they support animation (e.g. smooth the transition over 1 second)? That could work as a stop gap. Though humorously this would "solve" the periodic complaints of location lag if we hooked it up to a source that could generate new locations at 60Hz 😂
- Ideally we could hook into the underlying MapLibre location engine (I think that's the type name) interface that alre ady exists and leverage the native camera modes (C++ level).
There was a problem hiding this comment.
The user location currently comes from NavigationUiState.location, which during active navigation is typically the snapped location from the trip state. That means the UI is not rendering the raw GPS position, but the map-matched/snapped position, and in practice this currently seems to update at roughly 1 Hz.
While navigating, the puck is rendered via a custom map overlay (NavigationPuckOverlay) rather than the built-in maplibre-compose location puck. I implemented it this way because the built-in puck is not flexible enough for the current navigation-style appearance (for example the arrow / bearing presentation). I am currently experimenting with interpolating / animating the overlay position itself, but that is not entirely straightforward.
For the camera, I am also trying to smooth tracking via CameraState.animateTo().
Outside active navigation, Ferrostar uses the regular maplibre-compose location puck. I checked the maplibre-compose implementation as well, and it is structurally similar in that it also just renders the latest location value without doing any special native interpolation magic.
There was a problem hiding this comment.
I improved this further in the migration branch.
The key fix is that the navigation puck and tracking camera now use the same shared route-relative displayed location/bearing while on-route. Previously, the camera was following a smoothed route bearing while the puck still had its own extra bearing smoothing / raw bearing fallback, which caused visible mismatch and step-like rotation.
Now the only smoothing happens in the shared displayed navigation location:
- position is smoothed as forward progress along the route
- bearing comes from the route tangent
That makes the snapped on-route behavior noticeably more stable and keeps puck/camera rotation aligned.
I already tested it in real life while driving. When going off the planned route, the location puck stays on the old route for a short time, and then switches to the newly calculated route.
That said, I see this as more of a stopgap than the final solution as well. The current implementation is still a Compose-layer approximation using a custom puck layer plus CameraState, whereas the old Android path relies on native MapLibre tracking behavior with snapped locations as input.
So for the long-term solution, I think we would still want an Android-native integration path in maplibre-compose that lets us use the underlying native location/tracking behavior again, instead of rebuilding it manually at the Compose layer.
Screen_recording_20260401_001833_short.mp4
There was a problem hiding this comment.
Note: There is still a rendering issue with a "shaking" navigation arrow when zooming in more, but that's probably only solvable with some kind of access to the native map (which I would target as a follow-up PR).
…ibre UI Introduces a route-snapping mechanism for the navigation puck and camera using a new `rememberDisplayedNavigationLocation` hook. This ensures the displayed position and bearing stay locked to the route geometry during navigation, reducing jitter from noisy GPS data.
|
I think maplibre/maplibre-compose#761 could be directly relevant for this migration, although more as an improvement to the current workaround than as the final solution. What it adds on the For this migration, the most relevant part seems to be the current navigation puck path, since that is now rendered as a GeoJSON source + layer in the migration branch. That looks like exactly the kind of source this flag is meant for. The practical effect should be lower latency between puck data updates and visible layer updates, which may help reduce or eliminate the visible jitter at higher zoom levels. Also, the screen recording in PR #761 is probably the most useful part for this discussion, because it shows the visual difference pretty directly. I'll try to integrate the changes into this PR and see whether they have a positive effect. Kudos to @hactar for pointing me to the existing issue. |
Summary
This PR migrates
ui-maplibrefrom the legacy Rallista MapLibre Compose integration to the officialorg.maplibre.compose:maplibre-compose-androidSDK. (#754)The main scope is Android phone/tablet Compose navigation UI. Android Auto is not fully migrated here and should follow in a separate PR.
Changes
ui-maplibreto the official MapLibre Compose Android artifactMapViewCameraAPI withNavigationMapStateLineLayerNotes
ui-maplibre-car-appremains a compatibility path for nowNavigationMapView.onMapReadyCallbackhas been removed in favor ofonMapLoadFinishedandonMapLoadFailedmaplibre-composeload callbacksComposableScreenandSurfaceGestureCallback-based host APIsNavigationMapStateusing the officialmaplibre-composeAPI, so behavior may still differ somewhat from the previous native/Ramani-based implementationLocationComponentRenderMode.GPSTesting
Screen recording
Screen_recording_20260327_105813.mp4