Skip to content

fix(android): honor LocationPuck scale for the default puck - #4274

Open
Kerumen wants to merge 1 commit into
rnmapbox:mainfrom
Kerumen:fix/android-default-puck-scale
Open

Kerumen wants to merge 1 commit into
rnmapbox:mainfrom
Kerumen:fix/android-default-puck-scale

Conversation

@Kerumen

@Kerumen Kerumen commented Aug 4, 2026

Copy link
Copy Markdown

The bug

LocationPuck's scale prop is documented as supporting zoom expressions, and it works on iOS — but on Android it is silently ignored unless you also supply custom puck images.

So an app using the stock puck (the common case) gets a user marker fixed at one screen size while every other element on the map scales with zoom. Zoomed out, the puck ends up dwarfing everything around it.

Why

RNMBXNativeUserLocation._apply() branches on images.isEmpty():

if (images.isEmpty()) {
    location2.locationPuck =
        makeDefaultLocationPuck2D(mContext, androidRenderMode ?: RenderMode.NORMAL)  // scale never passed
} else {
    location2.locationPuck = LocationPuck2D(
        ...
        scaleExpression = scale?.toJson()   // only here
    )
}

The custom-images branch passes scaleExpression; the default branch never receives it.

iOS has no such split — RNMBXNativeUserLocation.swift builds the configuration (defaulting it when no images are supplied) and then applies scale unconditionally afterwards:

var configuration: Puck2DConfiguration = images.isEmpty ?
  .makeDefault(showBearing: puckBearingEnabled) : Puck2DConfiguration(...)

if let scale = toDoubleValue(value: scale, name: "scale") {
  configuration.scale = scale
}

The fix

Thread an optional scaleExpression through makeDefaultLocationPuck2D.

LocationPuck2D already accepts it alongside drawables — the same shape LocationComponentManager uses. The new parameter is defaulted and makeDefaultLocationPuck2D has a single call site, so no other behaviour changes and the stock puck keeps its platform look.

12 insertions, 3 deletions, one file.

Reproducer & verification

A MapView with no puck images and a zoom-scaled scale:

<LocationPuck
  puckBearingEnabled={false}
  scale={['interpolate', ['linear'], ['zoom'], 3, 0.35, 11, 0.6, 14, 0.9, 18, 1.1]}
/>
  • Before: the puck renders at one fixed size at every zoom on Android. Identical code scales correctly on iOS.
  • After: the puck scales on Android, matching iOS.

Verified on an Android device (not just an emulator) in a real app, against 10.3.1; the same code is unchanged on main at 10.3.5.

Happy to add an example scene if you'd like one — it wasn't obvious where a puck-scale demo best fits alongside the existing user-location examples, so I left /example untouched rather than guess.

@Kerumen
Kerumen had a problem deploying to CI with Mapbox Tokens August 4, 2026 10:38 — with GitHub Actions Failure
@Kerumen
Kerumen had a problem deploying to CI with Mapbox Tokens August 4, 2026 10:38 — with GitHub Actions Failure
@Kerumen
Kerumen had a problem deploying to CI with Mapbox Tokens August 4, 2026 10:38 — with GitHub Actions Failure

mfazekas commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

@Kerumen thanks for the fix, and sorry it took so long to get to it.

Could you merge the latest main into your branch? We changed how CI runs for fork PRs (#4298), so the checks here can't run until the branch is updated. It merges cleanly, so it should be a quick one. Once CI is green we can get this in.

@Kerumen

Kerumen commented Oct 5, 2026

Copy link
Copy Markdown
Author

@mfazekas I've merged main on this branch. Let me know if you need anything else!

mfazekas commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

@Kerumen thanks for the fix and for merging main so quickly. One request: could you structure it like iOS, where the puck is built first and scale is applied once afterwards? That keeps makeDefaultLocationPuck2D unchanged and both platforms read the same:

val puck = if (images.isEmpty()) {
    makeDefaultLocationPuck2D(mContext, androidRenderMode ?: RenderMode.NORMAL)
} else {
    LocationPuck2D(
        topImage = images[PuckImagePart.TOP],
        bearingImage = images[PuckImagePart.BEARING],
        shadowImage = images[PuckImagePart.SHADOW],
    )
}
location2.locationPuck = scale?.let { puck.copy(scaleExpression = it.toJson()) } ?: puck

Behavior should be identical to your version, so a quick re-check on device is enough.

@mfazekas

mfazekas commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

@Kerumen thanks, the fix is right. Could we structure it like iOS instead, where the puck is built first and scale is applied once afterwards? LocationPuck2D is a data class, so copy() works, and makeDefaultLocationPuck2D can keep its original signature:

val puck = if (images.isEmpty()) {
    makeDefaultLocationPuck2D(mContext, androidRenderMode ?: RenderMode.NORMAL)
} else {
    LocationPuck2D(
        topImage = images[PuckImagePart.TOP],
        bearingImage = images[PuckImagePart.BEARING],
        shadowImage = images[PuckImagePart.SHADOW],
    )
}
location2.locationPuck = scale?.let { puck.copy(scaleExpression = it.toJson()) } ?: puck

That keeps scale handling in one place for both branches and matches RNMBXNativeUserLocation.swift. Behaviour should be the same as your version, but please re-check it on device after the change.

The default puck branch of _apply() never received scaleExpression, so
scale was ignored on Android unless custom puck images were set. Build
the puck first, then apply scale once through copy(), as
RNMBXNativeUserLocation.swift does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kerumen
Kerumen force-pushed the fix/android-default-puck-scale branch from 9b3851c to cc15c2d Compare October 5, 2026 10:07
@Kerumen

Kerumen commented Oct 5, 2026

Copy link
Copy Markdown
Author

@mfazekas Restructured as suggested: the puck is built first and scale is applied once via copy(), so makeDefaultLocationPuck2D keeps its original signature. Re-checked on an Android device: the default puck scales with zoom.

This branch had an error being deployed

1 failed (outdated) and 1 active deployments
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.

2 participants