Skip to content

fix(android): defer initial camera events until defaults are applied - #4348

Closed
RobJacobson wants to merge 1 commit into
rnmapbox:mainfrom
RobJacobson:codex/fix-android-initial-camera-events
Closed

RobJacobson wants to merge 1 commit into
rnmapbox:mainfrom
RobJacobson:codex/fix-android-initial-camera-events

Conversation

@RobJacobson

Copy link
Copy Markdown
Contributor

Description

Fixes #4213

On Android, onCameraChanged can report [0, 0] before the child Camera applies defaultSettings. The native camera listener is registered during map creation, while the initial camera is applied through a callback posted after window attachment.

Hold startup camera notifications until lifecycle.afterAttachFromLooper() finishes applying the initial camera. If any camera changes occurred during initialization, emit one notification using the current camera state. Subsequent notifications follow the existing path. This also defers the legacy onRegionIsChanging callback; maps without a Camera still become ready, and configured or later moves to [0, 0] still produce callbacks.

Validation

Verified in the example app on the Medium_Phone Android emulator with Fabric, React Native 0.84.1, Mapbox Maps SDK 11.23.1, and the 10.3.8 source tree. The issue reports React Native 0.83.6 and SDK 11.20.1; those exact versions were not rerun.

  • The unfixed reproducer emitted an initial JavaScript camera callback at [0, 0], zoom 3. Native logging showed two camera changes before defaultSettings was applied.
  • After the fix, all four launches emitted their first camera callback at [-93.265, 44.9778], zoom 6. No camera callback at [0, 0] occurred. The idle camera was correct in all four runs.
  • Seven additional scenarios passed, with a fresh map mount between scenarios:
Scenario Result
Empty style with defaultSettings Initial camera and idle at the requested center
defaultSettings centered at [0, 0] Valid zero-coordinate camera callback delivered
Map without a Camera Camera and idle callbacks delivered
Camera without defaultSettings Camera and idle callbacks delivered
Controlled camera props alongside defaultSettings Controlled center and zoom take precedence
Later instant move to [0, 0] Updated camera and idle callbacks delivered
Later instant move to Tokyo Updated camera and idle callbacks delivered
  • Android example build and all 11 native unit tests passed: :app:assembleDebug :rnmapbox_maps:testDebugUnitTest -PreactNativeArchitectures=arm64-v8a.
  • yarn generate completed with no generated changes. yarn lint completed with existing warnings and no errors.
  • The initial yarn test run passed 181 Jest cases and failed the UserLocation case “renders with CircleLayers by default” with “No instances found.” All 14 UserLocation cases passed in isolation; the full serial rerun, yarn unittest --runInBand, passed all 182 cases across 27 suites.

Temporary reproducer and diagnostic logging were removed from the code change. The diff changes only RNMBXMapView.kt.

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
    • In New Architecture mode/ios
    • In V11 mode/android
    • In New Architecture mode/android
  • I added/updated a sample - if a new feature was implemented (/example)

iOS was not run. The new-feature sample checkbox is not applicable to this bug fix; the reproducer is included below.

Screenshot OR Video

This changes event ordering rather than visual layout. The captured event traces provide the before/after evidence.

Before and after camera events

Relevant fields from the recorded logs are shown below; bounds and process metadata are omitted. The timestamps are preserved.

Unfixed native trace:

18:26:26.565 SDK_CAMERA          center=[0.0, 0.0]        zoom=3.0
18:26:26.568 SDK_CAMERA          center=[0.0, 0.0]        zoom=3.0
18:26:26.571 APPLY_DEFAULT_BEGIN center=[0.0, 0.0]        zoom=3.0
18:26:26.571 SDK_CAMERA          center=[-93.265, 44.9778] zoom=6.0
18:26:26.571 APPLY_DEFAULT_END   center=[-93.265, 44.9778] zoom=6.0

Unfixed JavaScript callbacks:

18:26:26.587 camera center=[0,0]            zoom=3
18:26:27.056 idle   center=[-93.265,44.9778] zoom=6

Fixed JavaScript callbacks, first launch:

18:26:39.544 camera center=[-93.265,44.9778] zoom=6
18:26:39.959 idle   center=[-93.265,44.9778] zoom=6

All four fixed launches:

Launch First camera center First camera zoom Initial zero-coordinate callbacks
1 [-93.265,44.9778] 6 0
2 [-93.265,44.9778] 6 0
3 [-93.265,44.9778] 6 0
4 [-93.265,44.9778] 6 0

Component to reproduce the issue you're fixing

Replace example/src/examples/BugReportExample.js with the component below, rebuild the Android app, open the Bug Report example, and capture its camera and idle logs. The example app supplies its usual Mapbox access token. Repeat the launch on an unfixed build and on this branch.

This is the issue's satellite map and camera configuration with logging of all camera states and an overlay that does not change the map's layout. On the unfixed build, the first camera callback can be [0, 0]; with the fix, it is [-93.265, 44.9778] at zoom 6.

Standalone BugReportExample reproducer
import React, { useRef, useState } from 'react';
import { Text, View } from 'react-native';
import Mapbox, { Camera, MapView } from '@rnmapbox/maps';

export default function BugReportExample() {
  const sequence = useRef(0);
  const [events, setEvents] = useState([]);

  const record = (kind, state) => {
    const line = `${++sequence.current} ${kind} ${JSON.stringify(
      state?.properties,
    )}`;
    console.warn(`ISSUE4213 ${line}`);
    setEvents((previous) => [...previous.slice(-10), line]);
  };

  return (
    <View style={{ flex: 1 }}>
      <MapView
        style={{ flex: 1 }}
        styleURL={Mapbox.StyleURL.Satellite}
        onCameraChanged={(state) => record('camera', state)}
        onMapIdle={(state) => record('idle', state)}
        onDidFinishLoadingMap={() => record('loaded')}
      >
        <Camera
          defaultSettings={{
            centerCoordinate: [-93.265, 44.9778],
            zoomLevel: 6,
          }}
        />
      </MapView>
      <Text
        pointerEvents="none"
        style={{ position: 'absolute', top: 30, backgroundColor: 'white' }}
      >
        {events.join('\n')}
      </Text>
    </View>
  );
}

- Hold startup camera notifications until attachment callbacks finish applying the initial camera
- Emit the current camera once after initialization and preserve subsequent camera events
- Keep maps without a Camera and valid zero-coordinate camera positions working
@RobJacobson

Copy link
Copy Markdown
Contributor Author

Closing this duplicate in favor of #4355. That PR carries the same Android startup camera fix, adds focused CameraChangeEventGate tests, and now includes the validation record for this implementation family.

@RobJacobson
RobJacobson deleted the codex/fix-android-initial-camera-events branch October 14, 2026 00:01

This branch was successfully deployed

1 active 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]: Android: onCameraChanged fires with [0,0] on launch

1 participant