Skip to content

Fix Android issue with isUserInteraction always returning true when location puck is pulsing - #4222

Open
KijongHan wants to merge 7 commits into
rnmapbox:mainfrom
KijongHan:fix/issue-with-isUserInteraction-always-returning-true-when-location-puck-is-pulsing
Open

KijongHan wants to merge 7 commits into
rnmapbox:mainfrom
KijongHan:fix/issue-with-isUserInteraction-always-returning-true-when-location-puck-is-pulsing

Conversation

@KijongHan

Copy link
Copy Markdown
Contributor

Description

Fixes #3805

Fixes Android bug where onRegionIsChanging's properties.isUserInteraction was stuck true whenever <LocationPuck pulsing="default" /> was mounted, even if there were no user action. Root cause: the LocationComponent's pulsing animator keeps the map perpetually non-idle (as reported here), so Mapbox's MapIdle event never fires.

This means that CameraChangeTracker never cleared user gesture state as addOnMapIdleListener never fired if that <LocationPuck /> component was mounted. Fix was to use the new custom MapSteady event already introduced here #4074

Changes

  • Update CameraChangeTracker and replace single reason: CameraChangeReason field with reasonByGesture: Map<MapGestureType, CameraChangeReason> so concurrent move/scale/rotate gestures don't cause race conditions.
  • Decouple MapSteadyDetector from RNMBXCameraGestureObserver and use this custom event from RNMBXMapView to drive clearing the CameraChangeTracker state which corrects the downstream propagation to properties.isUserInteraction
  • Minor refactor so MapGestureType is promoted to shared enum

Checklist

  • I've read CONTRIBUTING.md
  • I updated the doc/other generated code with running yarn generate in the root folder
  • I have tested the new feature on /example app.
    • In V11 mode/ios (no, but shouldn't be affected)
    • In New Architecture mode/ios (no, but shouldn't be affected)
    • In V11 mode/android
    • In New Architecture mode/android
  • I added/updated a sample - if a new feature was implemented (/example)

Component to reproduce the issue you're fixing

Just pasting the same repro code snippet provided by the original issue

import { useEffect, useRef } from 'react';
import {
    MapView,
    ShapeSource,
    LineLayer,
    Camera,
    LocationPuck,
} from '@rnmapbox/maps';
import type { CameraRef } from '../../../src/components/Camera';

const aLine = {
    type: 'LineString',
    coordinates: [
        [-74.00597, 40.71427],
        [-74.00697, 40.71527],
    ],
}

const BugReportExample = () => {

    const mapCameraRef = useRef<CameraRef>(null)
    const locationPuck = true // Set this to false to hide the location puck. The issue only presents itself when the locationPuck is shown (and more specifically when this is set pulsing={"default"})

    useEffect(() => {
        // Re-center the map every 5 seconds. When this is done, isUserInteraction should log as false (but it doesn't when locationPuck is true)
        const interval = setInterval(() => {
            mapCameraRef.current?.setCamera({
                centerCoordinate: [-74.00597, 40.71427],
                animationDuration: 1000,
                animationMode: 'linearTo',
            })
        }, 5_000)

        return () => {
            clearInterval(interval)
        }

    }, [])

    const regionIsChanging = (region: any) => {
        console.log("isUserInteraction:", region.properties.isUserInteraction)
    }

    return (
        <MapView style={{ flex: 1 }} onRegionIsChanging={regionIsChanging}>
            <Camera ref={mapCameraRef} centerCoordinate={[-74.00597, 40.71427]} zoomLevel={14} />
            <ShapeSource id="idStreetLayer" shape={aLine as any}>
                <LineLayer id="idStreetLayer" />
            </ShapeSource>
            {locationPuck ? <LocationPuck puckBearingEnabled={true} puckBearing="heading" pulsing={"default"} /> : null}
        </MapView>
    )
}

export default BugReportExample;

@KijongHan
KijongHan temporarily deployed to CI with Mapbox Tokens June 1, 2026 22:36 — with GitHub Actions Inactive
@KijongHan
KijongHan had a problem deploying to CI with Mapbox Tokens June 1, 2026 22:36 — with GitHub Actions Failure
@KijongHan
KijongHan temporarily deployed to CI with Mapbox Tokens June 1, 2026 22:36 — with GitHub Actions Inactive
@KijongHan KijongHan changed the title Fix issue with isUserInteraction always returning true when location puck is pulsing Fix Android issue with isUserInteraction always returning true when location puck is pulsing Jun 1, 2026
@mfazekas

Copy link
Copy Markdown
Contributor

@KijongHan thanks much for the PR.

I'd rather add onRegionIsChanging or similar even to CameraGestureObserver. And kinda deprecate onRegionIsChanging

… gestures being tracked that could lead to race conditions
…t map steady event detection is decoupled and can be minimally reused from RNMBXMapView
…ent kotlin components

- refactor MapSteadyDetector so that it uses enum instead of magic strings
@KijongHan

Copy link
Copy Markdown
Contributor Author

@KijongHan thanks much for the PR.

I'd rather add onRegionIsChanging or similar even to CameraGestureObserver. And kinda deprecate onRegionIsChanging

thanks for reviewing these changes @mfazekas !

Just wanted to get some clarification on your suggestion. Do you mean deprecate onRegionIsChanging or onMapSteady?

If you mean the former, do you mean that we want to keep the underlying onRegionIsChanging event listener in CameraGestureObserver for internal logic but deprecate it from the javascript/React code?

@mfazekas

Copy link
Copy Markdown
Contributor

@KijongHan thanks much for the PR.
I'd rather add onRegionIsChanging or similar even to CameraGestureObserver. And kinda deprecate onRegionIsChanging

thanks for reviewing these changes @mfazekas !

Just wanted to get some clarification on your suggestion. Do you mean deprecate onRegionIsChanging or onMapSteady?

If you mean the former, do you mean that we want to keep the underlying onRegionIsChanging event listener in CameraGestureObserver for internal logic but deprecate it from the javascript/React code?

To clarify: deprecate onRegionIsChanging (the MapView event), not onMapSteady.

RNMBXMapView is already too complicated and the long-term plan is to split it into components — so I'd rather move code out of MapView, not into it. Concretely: CameraGestureObserver gains an onRegionIsChanging-like event (say onCameraChange) reporting isUserInteraction from its own gesture/animator tracking. Since it doesn't depend on MapIdle, it's immune to the pulsing puck issue. The MapView event stays as-is natively and gets deprecated in the JS API, so #3805 is addressed by migrating to the new event. Extracting MapSteadyDetector as a helper is fine — it should just stay owned by CameraGestureObserver, and the CameraChangeTracker changes wouldn't be needed.

KijongHan and others added 4 commits July 19, 2026 10:19
The MapSteadyDetector instance in RNMBXMapView only reset the camera
change reason to NONE and is no longer needed. Remove it and its import.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new onMapCameraChange event that emits a camera snapshot
(center, bounds, zoom, heading, pitch, isUserInteraction, timestamp)
on every camera change.

- add MapCameraChangeDetector to derive the change reason from live
  gesture/animation state and forward CameraChanged events
- add MapCameraChangeEvent to serialize the snapshot into the JS shape
- wire hasOnMapCameraChange prop and onMapCameraChange event through the
  observer, view manager, JS wrapper and codegen spec

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- add onMapCameraChange example under CameraGestureObserver
@KijongHan
KijongHan force-pushed the fix/issue-with-isUserInteraction-always-returning-true-when-location-puck-is-pulsing branch from 928330f to 9699111 Compare July 31, 2026 01:52
@KijongHan
KijongHan had a problem deploying to CI with Mapbox Tokens July 31, 2026 01:52 — with GitHub Actions Failure
@KijongHan
KijongHan had a problem deploying to CI with Mapbox Tokens July 31, 2026 01:52 — with GitHub Actions Failure
@KijongHan
KijongHan had a problem deploying to CI with Mapbox Tokens July 31, 2026 01:52 — with GitHub Actions Failure
@KijongHan

Copy link
Copy Markdown
Contributor Author

I see, @mfazekas. I think I understand your reasoning here. I created a parallel MapCameraChangeDetector.kt and accompanying MapCameraChangeEvent event. The new onMapCameraChange event handler subscribes to the underlying mapboxMap.subscribeCameraChanged native event.

I extended the example for CameraGestureObserver and added the above handler and an auto-center effect to default coordinates and outputs the isUserInteraction event property. I can see that the reported issue is resolved using this new event handler.

One thing to note Im sure you're aware is that the iOS implementation is not done here. And because we are extending on CameraGestureObserver (instead of extending on the existing MapView event handler) we need to introduce it to iOS too. I'm keen to get your eyes on these sets of changes to see if Im on the right track here

mfazekas commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

@KijongHan thanks, and sorry for the slow reply. Yes, this is the direction I meant. A few things before we merge:

  1. Please name it onCameraChanged, same as on MapView, and send the same MapState payload (properties, gestures, timestamp) instead of a new OnMapCameraChangeEvent. Then moving off the MapView prop is just moving the callback. The derived interaction state can go under gestures (e.g. gestures.isUserInteraction).
  2. In MapCameraChangeDetector, isGestureActive is a single boolean, so overlapping gestures (a pinch ending while a pan continues) flip it to false mid-gesture. Please track it per gesture type. activeAnimationOwner has the same issue with overlapping animations.
  3. detach() in both detectors should remove the gesture and animation listeners they add.
  4. The MapSteadyDetector extraction is fine, but please keep the behavior identical: on main the timeout also cancels the pending quiet check.
  5. Showing the new event in the CameraGestureObserver example is good, since examples are our docs. The auto-recenter timer is repro code though, so please move that into a small reproducer component in the PR description (pulsing LocationPuck plus the periodic setCamera from [Bug]: isUserInteraction is always true when locationPuck is shown on Android #3805), and import CameraRef from @rnmapbox/maps rather than src.
  6. iOS: implement it in RNMBXCameraGestureObserver.swift, or document it as Android-only for now. We'll deprecate MapView's onRegionIsChanging once both platforms have it.

This branch had an error being deployed

1 failed deployment
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.

[Bug]: isUserInteraction is always true when locationPuck is shown on Android

2 participants