From 3cc1e665866ad875be81be6ba694afcdb4c35ad6 Mon Sep 17 00:00:00 2001 From: Christian Bernier Date: Tue, 17 Mar 2026 16:20:23 -0400 Subject: [PATCH] feat: add onCameraFollowLocationChanged callback to NavigationView Add a new `onCameraFollowLocationChanged` event prop to `NavigationView` that fires whenever the camera enters or exits follow-my-location mode. **Android:** Uses `GoogleMap.setOnFollowMyLocationCallback` to emit events when the camera starts/stops following the user's location. **iOS:** Tracks `GMSNavigationCameraMode` changes via GMSMapView delegate methods (`willMove:`, `idleAtCameraPosition:`, `mapViewDidTapRecenterButton:`) and reports state transitions. Also fires on programmatic changes like `showRouteOverview` and `setFollowingPerspective`. This enables apps to show/hide a custom recenter button or adjust UI based on whether the map is actively tracking the user's position. Made-with: Cursor --- .../android/react/navsdk/NavViewFragment.java | 17 ++++++++++++ .../INavigationViewCallback.h | 1 + ios/react-native-navigation-sdk/NavView.mm | 5 ++++ .../NavViewController.mm | 26 +++++++++++++++++++ src/native/NativeNavViewComponent.ts | 1 + .../navigationView/navigationView.tsx | 10 +++++++ src/navigation/navigationView/types.ts | 8 ++++++ 7 files changed, 68 insertions(+) diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java index 16c734b6..f33baf49 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java @@ -86,6 +86,23 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat applyMapColorSchemeToMap(); applyNightModePreference(); + googleMap.setOnFollowMyLocationCallback( + new GoogleMap.OnCameraFollowLocationCallback() { + @Override + public void onCameraStartedFollowingLocation() { + WritableMap map = Arguments.createMap(); + map.putBoolean("isFollowing", true); + emitEvent("onCameraFollowLocationChanged", map); + } + + @Override + public void onCameraStoppedFollowingLocation() { + WritableMap map = Arguments.createMap(); + map.putBoolean("isFollowing", false); + emitEvent("onCameraFollowLocationChanged", map); + } + }); + emitEvent("onMapReady", null); // Request layout to ensure fragment is properly sized diff --git a/ios/react-native-navigation-sdk/INavigationViewCallback.h b/ios/react-native-navigation-sdk/INavigationViewCallback.h index de2aa684..0a28a48e 100644 --- a/ios/react-native-navigation-sdk/INavigationViewCallback.h +++ b/ios/react-native-navigation-sdk/INavigationViewCallback.h @@ -35,6 +35,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)handleCircleClick:(GMSCircle *)circle; - (void)handleGroundOverlayClick:(GMSGroundOverlay *)groundOverlay; - (void)handlePromptVisibilityChanged:(BOOL)isVisible; +- (void)handleCameraFollowLocationChanged:(BOOL)isFollowing; @end NS_ASSUME_NONNULL_END diff --git a/ios/react-native-navigation-sdk/NavView.mm b/ios/react-native-navigation-sdk/NavView.mm index cac635d3..97acfe04 100644 --- a/ios/react-native-navigation-sdk/NavView.mm +++ b/ios/react-native-navigation-sdk/NavView.mm @@ -415,6 +415,11 @@ - (void)handleCircleClick:(GMSCircle *)circle { self.eventEmitter.onCircleClick(result); } +- (void)handleCameraFollowLocationChanged:(BOOL)isFollowing { + NavViewEventEmitter::OnCameraFollowLocationChanged result = {isFollowing}; + self.eventEmitter.onCameraFollowLocationChanged(result); +} + - (void)handleGroundOverlayClick:(GMSGroundOverlay *)groundOverlay { NavViewEventEmitter::OnGroundOverlayClick result = { [ObjectTranslationUtil isIdOnUserData:groundOverlay.userData] diff --git a/ios/react-native-navigation-sdk/NavViewController.mm b/ios/react-native-navigation-sdk/NavViewController.mm index 059e5d5e..3326b2f2 100644 --- a/ios/react-native-navigation-sdk/NavViewController.mm +++ b/ios/react-native-navigation-sdk/NavViewController.mm @@ -37,6 +37,7 @@ @implementation NavViewController { MapViewType *_mapViewType; // Nullable - must be set before loadView id _viewCallbacks; BOOL _isSessionAttached; + BOOL _lastReportedIsFollowing; NSNumber *_isNavigationUIEnabled; NSNumber *_navigationUIEnabledPreference; // 0=AUTOMATIC, 1=DISABLED NSNumber *_navigationLightingMode; @@ -270,6 +271,29 @@ - (void)dealloc { - (void)mapViewDidTapRecenterButton:(GMSMapView *)mapView { [_viewCallbacks handleRecenterButtonClick]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self reportFollowingStateIfChanged]; + }); +} + +- (void)mapView:(GMSMapView *)mapView willMove:(BOOL)gesture { + if (gesture) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self reportFollowingStateIfChanged]; + }); + } +} + +- (void)mapView:(GMSMapView *)mapView idleAtCameraPosition:(GMSCameraPosition *)position { + [self reportFollowingStateIfChanged]; +} + +- (void)reportFollowingStateIfChanged { + BOOL isFollowing = (_mapView.cameraMode == GMSNavigationCameraModeFollowing); + if (_lastReportedIsFollowing != isFollowing) { + _lastReportedIsFollowing = isFollowing; + [_viewCallbacks handleCameraFollowLocationChanged:isFollowing]; + } } - (void)mapView:(GMSMapView *)mapView didTapInfoWindowOfMarker:(GMSMarker *)marker { @@ -518,6 +542,7 @@ - (void)setNightMode:(NSNumber *)index { - (void)showRouteOverview { _mapView.cameraMode = GMSNavigationCameraModeOverview; + [self reportFollowingStateIfChanged]; } - (void)setTripProgressBarEnabled:(BOOL)isEnabled { @@ -569,6 +594,7 @@ - (void)setFollowingPerspective:(NSNumber *)index { [_mapView setFollowingPerspective:GMSNavigationCameraPerspectiveTilted]; } _mapView.cameraMode = GMSNavigationCameraModeFollowing; + [self reportFollowingStateIfChanged]; } - (void)setSpeedometerEnabled:(BOOL)isEnabled { diff --git a/src/native/NativeNavViewComponent.ts b/src/native/NativeNavViewComponent.ts index a03da680..2b423952 100644 --- a/src/native/NativeNavViewComponent.ts +++ b/src/native/NativeNavViewComponent.ts @@ -194,6 +194,7 @@ export interface NativeNavViewProps extends ViewProps { }>; onRecenterButtonClick?: DirectEventHandler; onPromptVisibilityChanged?: DirectEventHandler<{ visible: boolean }>; + onCameraFollowLocationChanged?: DirectEventHandler<{ isFollowing: boolean }>; } export type NativeNavViewType = HostComponent; diff --git a/src/navigation/navigationView/navigationView.tsx b/src/navigation/navigationView/navigationView.tsx index 22f4d159..0a0e6c17 100644 --- a/src/navigation/navigationView/navigationView.tsx +++ b/src/navigation/navigationView/navigationView.tsx @@ -221,6 +221,15 @@ export const NavigationView = ( [onPromptVisibilityChangedProp] ); + const { onCameraFollowLocationChanged: onCameraFollowLocationChangedProp } = + props; + const onCameraFollowLocationChanged = useCallback( + (event: { nativeEvent: { isFollowing: boolean } }) => { + onCameraFollowLocationChangedProp?.(event.nativeEvent.isFollowing); + }, + [onCameraFollowLocationChangedProp] + ); + return ( ); }; diff --git a/src/navigation/navigationView/types.ts b/src/navigation/navigationView/types.ts index 5ac5d4bb..f85eb8ad 100644 --- a/src/navigation/navigationView/types.ts +++ b/src/navigation/navigationView/types.ts @@ -66,6 +66,14 @@ export interface NavigationViewProps extends MapViewProps { */ readonly onRecenterButtonClick?: () => void; + /** + * Callback invoked when the camera enters or exits follow-my-location mode. + * + * @param isFollowing - True when the camera is following the user's location, + * false when the user has panned or zoomed away. + */ + readonly onCameraFollowLocationChanged?: (isFollowing: boolean) => void; + /** * A callback function invoked before a Navigation SDK UI prompt * element is about to appear and as soon as the element is removed.