From 9b5fe2120b7cdd233ba86b74013bd80557623a37 Mon Sep 17 00:00:00 2001 From: Joonas Kerttula Date: Fri, 15 May 2026 12:16:09 +0300 Subject: [PATCH] fix: prop handling on native Android implementation --- ...a => MapViewControllerPropertiesSink.java} | 164 +----------------- .../android/react/navsdk/MapViewFragment.java | 10 +- .../react/navsdk/MapViewPropertiesSink.java | 162 +++++++++++++++++ .../android/react/navsdk/NavViewFragment.java | 10 +- .../android/react/navsdk/NavViewManager.java | 97 +++++++---- 5 files changed, 247 insertions(+), 196 deletions(-) rename android/src/main/java/com/google/android/react/navsdk/{ViewPropertiesSink.java => MapViewControllerPropertiesSink.java} (57%) create mode 100644 android/src/main/java/com/google/android/react/navsdk/MapViewPropertiesSink.java diff --git a/android/src/main/java/com/google/android/react/navsdk/ViewPropertiesSink.java b/android/src/main/java/com/google/android/react/navsdk/MapViewControllerPropertiesSink.java similarity index 57% rename from android/src/main/java/com/google/android/react/navsdk/ViewPropertiesSink.java rename to android/src/main/java/com/google/android/react/navsdk/MapViewControllerPropertiesSink.java index 7ada54d0..6f451e27 100644 --- a/android/src/main/java/com/google/android/react/navsdk/ViewPropertiesSink.java +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewControllerPropertiesSink.java @@ -17,19 +17,9 @@ package com.google.android.react.navsdk; import androidx.annotation.Nullable; -import com.google.android.libraries.navigation.StylingOptions; -/** - * Unified property sink for buffering both fragment-level and controller-level properties that - * arrive before the fragment is created. Implements both INavigationViewProperties and - * INavigationViewControllerProperties to handle all property types in a single object. - */ -public class ViewPropertiesSink - implements INavigationViewProperties, INavigationViewControllerProperties { - // Map fragment properties - @Nullable private Integer mapColorScheme; - - // Map controller properties +/** Buffers map-controller props until getMapAsync creates the MapViewController. */ +public class MapViewControllerPropertiesSink implements INavigationViewControllerProperties { @Nullable private Integer mapType; @Nullable private Integer paddingTop; @Nullable private Integer paddingLeft; @@ -53,28 +43,6 @@ public class ViewPropertiesSink @Nullable private Float minZoomLevel; @Nullable private Float maxZoomLevel; - // Navigation fragment properties - @Nullable private Integer nightModeOption; - @Nullable private Boolean tripProgressBarEnabled; - @Nullable private Boolean trafficPromptsEnabled; - @Nullable private Boolean trafficIncidentCardsEnabled; - @Nullable private Boolean headerEnabled; - @Nullable private Boolean footerEnabled; - @Nullable private Boolean speedometerEnabled; - @Nullable private Boolean speedLimitIconEnabled; - @Nullable private Boolean recenterButtonEnabled; - @Nullable private Boolean reportIncidentButtonEnabled; - @Nullable private StylingOptions stylingOptions; - - // ========== IMapViewProperties implementation ========== - - @Override - public void setMapColorScheme(int colorScheme) { - this.mapColorScheme = colorScheme; - } - - // ========== IMapViewControllerProperties implementation ========== - @Override public void setMapType(int mapType) { this.mapType = mapType; @@ -173,64 +141,6 @@ public void setMaxZoomLevel(float maxZoomLevel) { this.maxZoomLevel = maxZoomLevel; } - // ========== INavigationViewProperties implementation ========== - - @Override - public void setNightModeOption(int nightMode) { - this.nightModeOption = nightMode; - } - - @Override - public void setStylingOptions(StylingOptions options) { - this.stylingOptions = options; - } - - @Override - public void setTripProgressBarEnabled(boolean enabled) { - this.tripProgressBarEnabled = enabled; - } - - @Override - public void setTrafficPromptsEnabled(boolean enabled) { - this.trafficPromptsEnabled = enabled; - } - - @Override - public void setTrafficIncidentCardsEnabled(boolean enabled) { - this.trafficIncidentCardsEnabled = enabled; - } - - @Override - public void setHeaderEnabled(boolean enabled) { - this.headerEnabled = enabled; - } - - @Override - public void setFooterEnabled(boolean enabled) { - // Footer on Android is the same as ETA card - this.footerEnabled = enabled; - } - - @Override - public void setSpeedometerEnabled(boolean enabled) { - this.speedometerEnabled = enabled; - } - - @Override - public void setSpeedLimitIconEnabled(boolean enabled) { - this.speedLimitIconEnabled = enabled; - } - - @Override - public void setRecenterButtonEnabled(boolean enabled) { - this.recenterButtonEnabled = enabled; - } - - @Override - public void setReportIncidentButtonEnabled(boolean enabled) { - this.reportIncidentButtonEnabled = enabled; - } - /** Apply all buffered controller properties to the map controller. */ public void applyToController(MapViewController controller) { if (controller == null) { @@ -300,74 +210,8 @@ public void applyToController(MapViewController controller) { } } - /** Apply all buffered fragment properties to the fragment. */ - public void applyToFragment(IMapViewFragment fragment) { - if (fragment == null) { - return; - } - - // Apply map fragment properties - if (mapColorScheme != null) { - fragment.setMapColorScheme(mapColorScheme); - } - - // Apply navigation fragment properties if applicable - if (fragment instanceof INavViewFragment) { - INavViewFragment navFragment = (INavViewFragment) fragment; - - if (nightModeOption != null) { - navFragment.setNightModeOption(nightModeOption); - } - if (stylingOptions != null) { - navFragment.setStylingOptions(stylingOptions); - } - if (tripProgressBarEnabled != null) { - navFragment.setTripProgressBarEnabled(tripProgressBarEnabled); - } - if (trafficPromptsEnabled != null) { - navFragment.setTrafficPromptsEnabled(trafficPromptsEnabled); - } - if (trafficIncidentCardsEnabled != null) { - navFragment.setTrafficIncidentCardsEnabled(trafficIncidentCardsEnabled); - } - if (headerEnabled != null) { - navFragment.setHeaderEnabled(headerEnabled); - } - if (footerEnabled != null) { - navFragment.setFooterEnabled(footerEnabled); - } - if (speedometerEnabled != null) { - navFragment.setSpeedometerEnabled(speedometerEnabled); - } - if (speedLimitIconEnabled != null) { - navFragment.setSpeedLimitIconEnabled(speedLimitIconEnabled); - } - if (recenterButtonEnabled != null) { - navFragment.setRecenterButtonEnabled(recenterButtonEnabled); - } - if (reportIncidentButtonEnabled != null) { - navFragment.setReportIncidentButtonEnabled(reportIncidentButtonEnabled); - } - } - } - - /** Clear all buffered properties (both fragment and controller). */ + /** Clear all buffered properties. */ public void clear() { - // Clear fragment properties - mapColorScheme = null; - nightModeOption = null; - stylingOptions = null; - tripProgressBarEnabled = null; - trafficPromptsEnabled = null; - trafficIncidentCardsEnabled = null; - headerEnabled = null; - footerEnabled = null; - speedometerEnabled = null; - speedLimitIconEnabled = null; - recenterButtonEnabled = null; - reportIncidentButtonEnabled = null; - - // Clear controller properties mapType = null; paddingTop = null; paddingLeft = null; @@ -388,5 +232,7 @@ public void clear() { tiltGesturesEnabled = null; zoomGesturesEnabled = null; zoomControlsEnabled = null; + minZoomLevel = null; + maxZoomLevel = null; } } diff --git a/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java index a0731ea6..06814d00 100644 --- a/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java @@ -47,10 +47,14 @@ public class MapViewFragment extends SupportMapFragment private ReactApplicationContext reactContext; private GoogleMap mGoogleMap; private MapViewController mMapViewController; + @Nullable private MapViewControllerReadyListener mapViewControllerReadyListener; private @MapColorScheme int mapColorScheme = MapColorScheme.FOLLOW_SYSTEM; public static MapViewFragment newInstance( - ReactApplicationContext reactContext, int viewTag, @NonNull GoogleMapOptions mapOptions) { + ReactApplicationContext reactContext, + int viewTag, + @NonNull GoogleMapOptions mapOptions, + @Nullable MapViewControllerReadyListener mapViewControllerReadyListener) { MapViewFragment fragment = new MapViewFragment(); Bundle args = new Bundle(); args.putParcelable("MapOptions", mapOptions); @@ -58,6 +62,7 @@ public static MapViewFragment newInstance( fragment.setArguments(args); fragment.reactContext = reactContext; fragment.viewTag = viewTag; + fragment.mapViewControllerReadyListener = mapViewControllerReadyListener; return fragment; } @@ -76,6 +81,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat // Setup map listeners with the provided callback mMapViewController.setupMapListeners(MapViewFragment.this); + if (mapViewControllerReadyListener != null) { + mapViewControllerReadyListener.onMapViewControllerReady(viewTag); + } applyMapColorSchemeToMap(); emitEvent("onMapReady", null); diff --git a/android/src/main/java/com/google/android/react/navsdk/MapViewPropertiesSink.java b/android/src/main/java/com/google/android/react/navsdk/MapViewPropertiesSink.java new file mode 100644 index 00000000..e7f2b435 --- /dev/null +++ b/android/src/main/java/com/google/android/react/navsdk/MapViewPropertiesSink.java @@ -0,0 +1,162 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.react.navsdk; + +import androidx.annotation.Nullable; +import com.google.android.libraries.navigation.StylingOptions; + +/** Buffers fragment-level props until the native map or navigation fragment is attached. */ +public class MapViewPropertiesSink implements INavigationViewProperties { + @Nullable private Integer mapColorScheme; + @Nullable private Integer nightModeOption; + @Nullable private Boolean tripProgressBarEnabled; + @Nullable private Boolean trafficPromptsEnabled; + @Nullable private Boolean trafficIncidentCardsEnabled; + @Nullable private Boolean headerEnabled; + @Nullable private Boolean footerEnabled; + @Nullable private Boolean speedometerEnabled; + @Nullable private Boolean speedLimitIconEnabled; + @Nullable private Boolean recenterButtonEnabled; + @Nullable private Boolean reportIncidentButtonEnabled; + @Nullable private StylingOptions stylingOptions; + + @Override + public void setMapColorScheme(int colorScheme) { + this.mapColorScheme = colorScheme; + } + + @Override + public void setNightModeOption(int nightMode) { + this.nightModeOption = nightMode; + } + + @Override + public void setStylingOptions(StylingOptions options) { + this.stylingOptions = options; + } + + @Override + public void setTripProgressBarEnabled(boolean enabled) { + this.tripProgressBarEnabled = enabled; + } + + @Override + public void setTrafficPromptsEnabled(boolean enabled) { + this.trafficPromptsEnabled = enabled; + } + + @Override + public void setTrafficIncidentCardsEnabled(boolean enabled) { + this.trafficIncidentCardsEnabled = enabled; + } + + @Override + public void setHeaderEnabled(boolean enabled) { + this.headerEnabled = enabled; + } + + @Override + public void setFooterEnabled(boolean enabled) { + // Footer on Android is the same as ETA card. + this.footerEnabled = enabled; + } + + @Override + public void setSpeedometerEnabled(boolean enabled) { + this.speedometerEnabled = enabled; + } + + @Override + public void setSpeedLimitIconEnabled(boolean enabled) { + this.speedLimitIconEnabled = enabled; + } + + @Override + public void setRecenterButtonEnabled(boolean enabled) { + this.recenterButtonEnabled = enabled; + } + + @Override + public void setReportIncidentButtonEnabled(boolean enabled) { + this.reportIncidentButtonEnabled = enabled; + } + + /** Apply all buffered fragment properties to the fragment. */ + public void applyToFragment(IMapViewFragment fragment) { + if (fragment == null) { + return; + } + + if (mapColorScheme != null) { + fragment.setMapColorScheme(mapColorScheme); + } + + if (fragment instanceof INavViewFragment) { + INavViewFragment navFragment = (INavViewFragment) fragment; + + if (nightModeOption != null) { + navFragment.setNightModeOption(nightModeOption); + } + if (stylingOptions != null) { + navFragment.setStylingOptions(stylingOptions); + } + if (tripProgressBarEnabled != null) { + navFragment.setTripProgressBarEnabled(tripProgressBarEnabled); + } + if (trafficPromptsEnabled != null) { + navFragment.setTrafficPromptsEnabled(trafficPromptsEnabled); + } + if (trafficIncidentCardsEnabled != null) { + navFragment.setTrafficIncidentCardsEnabled(trafficIncidentCardsEnabled); + } + if (headerEnabled != null) { + navFragment.setHeaderEnabled(headerEnabled); + } + if (footerEnabled != null) { + navFragment.setFooterEnabled(footerEnabled); + } + if (speedometerEnabled != null) { + navFragment.setSpeedometerEnabled(speedometerEnabled); + } + if (speedLimitIconEnabled != null) { + navFragment.setSpeedLimitIconEnabled(speedLimitIconEnabled); + } + if (recenterButtonEnabled != null) { + navFragment.setRecenterButtonEnabled(recenterButtonEnabled); + } + if (reportIncidentButtonEnabled != null) { + navFragment.setReportIncidentButtonEnabled(reportIncidentButtonEnabled); + } + } + } + + /** Clear all buffered properties. */ + public void clear() { + mapColorScheme = null; + nightModeOption = null; + stylingOptions = null; + tripProgressBarEnabled = null; + trafficPromptsEnabled = null; + trafficIncidentCardsEnabled = null; + headerEnabled = null; + footerEnabled = null; + speedometerEnabled = null; + speedLimitIconEnabled = null; + recenterButtonEnabled = null; + reportIncidentButtonEnabled = null; + } +} 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 8e06b7de..510ccb27 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 @@ -51,11 +51,15 @@ public class NavViewFragment extends SupportNavigationFragment private MapViewController mMapViewController; private GoogleMap mGoogleMap; private StylingOptions mStylingOptions; + @Nullable private MapViewControllerReadyListener mapViewControllerReadyListener; private @MapColorScheme int mapColorScheme = MapColorScheme.FOLLOW_SYSTEM; private @ForceNightMode int nightModeOverride = ForceNightMode.AUTO; public static NavViewFragment newInstance( - ReactApplicationContext reactContext, int viewTag, @NonNull GoogleMapOptions mapOptions) { + ReactApplicationContext reactContext, + int viewTag, + @NonNull GoogleMapOptions mapOptions, + @Nullable MapViewControllerReadyListener mapViewControllerReadyListener) { NavViewFragment fragment = new NavViewFragment(); Bundle args = new Bundle(); args.putParcelable("MapOptions", mapOptions); @@ -63,6 +67,7 @@ public static NavViewFragment newInstance( fragment.setArguments(args); fragment.reactContext = reactContext; fragment.viewTag = viewTag; + fragment.mapViewControllerReadyListener = mapViewControllerReadyListener; return fragment; } @@ -83,6 +88,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat // Setup map listeners with the provided callback mMapViewController.setupMapListeners(NavViewFragment.this); + if (mapViewControllerReadyListener != null) { + mapViewControllerReadyListener.onMapViewControllerReady(viewTag); + } applyMapColorSchemeToMap(); applyNightModePreference(); diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java b/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java index 0090ae19..d2bc7004 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewManager.java @@ -62,9 +62,12 @@ public class NavViewManager extends SimpleViewManager // Track views with pending fragment creation attempts. private final HashSet pendingFragments = new HashSet<>(); - // Property sink to buffer both fragment and controller props that arrive before - // fragment is created - private final HashMap propertySinkMap = new HashMap<>(); + // React props can arrive before different native targets are ready. Fragment-level props can be + // applied once the Fragment is attached. Controller-level props must wait longer: getMapAsync + // creates MapViewController after fragment attachment, so keep them buffered separately. + private final HashMap fragmentPropertySinkMap = new HashMap<>(); + private final HashMap controllerPropertySinkMap = + new HashMap<>(); // nativeID-based view registry for TurboModule access private final HashMap> viewRegistry = new HashMap<>(); @@ -291,10 +294,14 @@ public void onDropViewInstance(@NonNull FrameLayout view) { Choreographer.getInstance().removeFrameCallback(frameCallback); } - // Clean up property sink - ViewPropertiesSink sink = propertySinkMap.remove(viewId); - if (sink != null) { - sink.clear(); + // Clean up property sinks + MapViewPropertiesSink fragmentSink = fragmentPropertySinkMap.remove(viewId); + if (fragmentSink != null) { + fragmentSink.clear(); + } + MapViewControllerPropertiesSink controllerSink = controllerPropertySinkMap.remove(viewId); + if (controllerSink != null) { + controllerSink.clear(); } FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity(); @@ -322,10 +329,10 @@ private INavigationViewProperties getNavigationFragmentProperties(int viewId) { } // Return sink to buffer properties - ViewPropertiesSink sink = propertySinkMap.get(viewId); + MapViewPropertiesSink sink = fragmentPropertySinkMap.get(viewId); if (sink == null) { - sink = new ViewPropertiesSink(); - propertySinkMap.put(viewId, sink); + sink = new MapViewPropertiesSink(); + fragmentPropertySinkMap.put(viewId, sink); } return sink; } @@ -339,10 +346,10 @@ private IMapViewProperties getMapFragmentProperties(int viewId) { } // Return sink to buffer properties - ViewPropertiesSink sink = propertySinkMap.get(viewId); + MapViewPropertiesSink sink = fragmentPropertySinkMap.get(viewId); if (sink == null) { - sink = new ViewPropertiesSink(); - propertySinkMap.put(viewId, sink); + sink = new MapViewPropertiesSink(); + fragmentPropertySinkMap.put(viewId, sink); } return sink; } @@ -356,30 +363,46 @@ private INavigationViewControllerProperties getMapControllerProperties(int viewI } // Return sink to buffer controller properties - ViewPropertiesSink sink = propertySinkMap.get(viewId); + MapViewControllerPropertiesSink sink = controllerPropertySinkMap.get(viewId); if (sink == null) { - sink = new ViewPropertiesSink(); - propertySinkMap.put(viewId, sink); + sink = new MapViewControllerPropertiesSink(); + controllerPropertySinkMap.put(viewId, sink); } return sink; } - /** - * Apply buffered properties from the unified sink to both fragment and controller, then discard - * the sink. This should be called once when the fragment is ready. - */ - private void applyBufferedPropertiesAndClearSinks(int viewId, IMapViewFragment fragment) { - ViewPropertiesSink sink = propertySinkMap.remove(viewId); - if (sink != null) { - // Apply fragment-level properties - sink.applyToFragment(fragment); - - // Apply controller-level properties - if (fragment.getMapController() != null) { - sink.applyToController(fragment.getMapController()); - } + /** Apply buffered fragment properties once the fragment is attached. */ + private void applyBufferedFragmentProperties(int viewId, IMapViewFragment fragment) { + MapViewPropertiesSink sink = fragmentPropertySinkMap.remove(viewId); - sink.clear(); + if (sink == null) { + return; + } + + sink.applyToFragment(fragment); + sink.clear(); + } + + /** Apply buffered controller properties once getMapAsync has created the controller. */ + private void applyBufferedControllerProperties(int viewId, IMapViewFragment fragment) { + MapViewController controller = fragment.getMapController(); + if (controller == null) { + return; + } + + MapViewControllerPropertiesSink sink = controllerPropertySinkMap.remove(viewId); + if (sink == null) { + return; + } + + sink.applyToController(controller); + sink.clear(); + } + + private void onMapViewControllerReady(int viewId) { + IMapViewFragment fragment = getFragmentForViewId(viewId); + if (fragment != null) { + applyBufferedControllerProperties(viewId, fragment); } } @@ -763,12 +786,14 @@ private void commitFragmentTransaction( if (mapViewType == CustomTypes.MapViewType.MAP) { MapViewFragment mapFragment = - MapViewFragment.newInstance(reactContext, viewId, googleMapOptions); + MapViewFragment.newInstance( + reactContext, viewId, googleMapOptions, this::onMapViewControllerReady); fragment = mapFragment; mapViewFragment = mapFragment; } else { NavViewFragment navFragment = - NavViewFragment.newInstance(reactContext, viewId, googleMapOptions); + NavViewFragment.newInstance( + reactContext, viewId, googleMapOptions, this::onMapViewControllerReady); if (viewInitializationParams.hasKey("navigationNightMode") && !viewInitializationParams.isNull("navigationNightMode")) { @@ -827,8 +852,10 @@ private void commitFragmentTransaction( mapOptionsCache.remove(viewId); fragmentMap.put(viewId, new WeakReference<>(mapViewFragment)); - // Apply any buffered properties that arrived before fragment was created - applyBufferedPropertiesAndClearSinks(viewId, mapViewFragment); + // Apply fragment properties that arrived before fragment creation. + // Controller properties stay buffered until getMapAsync creates the controller. + applyBufferedFragmentProperties(viewId, mapViewFragment); + applyBufferedControllerProperties(viewId, mapViewFragment); // Start per-frame layout loop to keep fragment sized correctly. startLayoutLoop(view);