Skip to content

Commit 6e0cd58

Browse files
authored
feat: support for setting map color schema (#500)
Adds a new mapColorScheme view property to allow controlling the map’s light and dark modes. Fixes night-mode handling on iOS, including the ability to reset night mode back to automatic. Deprecates the setNightMode method and moves night-mode handling to the navigationNightMode view property.
1 parent cc0aae1 commit 6e0cd58

24 files changed

+517
-82
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,15 @@ You can also add a bare `MapView` that works as a normal map view without naviga
317317
/>
318318
```
319319

320+
### Control light and dark modes
321+
322+
Use the `mapColorScheme` prop on both `NavigationView` and `MapView` to force the map tiles into light, dark, or system-following mode.
323+
324+
For the navigation UI, pass the `navigationNightMode` prop to `NavigationView` to configure the initial lighting mode for navigation session.
325+
326+
> [!NOTE]
327+
> When navigation UI is enabled, `mapColorScheme` does not affect the view styling. To control the style of the navigation UI, use the `navigationNightMode` prop on `NavigationView` instead.
328+
320329
### Requesting and handling permissions
321330

322331
The Google Navigation SDK React Native library offers functionalities that necessitate specific permissions from the mobile operating system. These include, but are not limited to, location services, background execution, and receiving background location updates.

android/src/main/java/com/google/android/react/navsdk/EnumTranslationUtil.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import com.google.android.gms.maps.GoogleMap;
1717
import com.google.android.gms.maps.GoogleMap.CameraPerspective;
18+
import com.google.android.gms.maps.model.MapColorScheme;
1819
import com.google.android.libraries.navigation.AlternateRoutesStrategy;
1920
import com.google.android.libraries.navigation.ForceNightMode;
2021
import com.google.android.libraries.navigation.Navigator;
@@ -96,4 +97,15 @@ public static CustomTypes.MapViewType getMapViewTypeFromJsValue(int jsValue) {
9697
default -> throw new IllegalStateException("Unexpected MapViewType value: " + jsValue);
9798
};
9899
}
100+
101+
public static @MapColorScheme int getMapColorSchemeFromJsValue(int jsValue) {
102+
switch (jsValue) {
103+
case 1:
104+
return MapColorScheme.LIGHT;
105+
case 2:
106+
return MapColorScheme.DARK;
107+
default:
108+
return MapColorScheme.FOLLOW_SYSTEM;
109+
}
110+
}
99111
}

android/src/main/java/com/google/android/react/navsdk/IMapViewFragment.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import android.view.View;
1717
import com.google.android.gms.maps.GoogleMap;
18+
import com.google.android.gms.maps.model.MapColorScheme;
1819

1920
public interface IMapViewFragment {
2021
MapViewController getMapController();
@@ -23,6 +24,8 @@ public interface IMapViewFragment {
2324

2425
GoogleMap getGoogleMap();
2526

27+
void setMapColorScheme(@MapColorScheme int colorScheme);
28+
2629
// Fragment
2730
boolean isAdded();
2831

android/src/main/java/com/google/android/react/navsdk/INavViewFragment.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414
package com.google.android.react.navsdk;
1515

16+
import com.google.android.libraries.navigation.ForceNightMode;
1617
import com.google.android.libraries.navigation.StylingOptions;
1718

1819
public interface INavViewFragment extends IMapViewFragment {
@@ -34,7 +35,7 @@ public interface INavViewFragment extends IMapViewFragment {
3435

3536
void showRouteOverview();
3637

37-
void setNightModeOption(int jsValue);
38+
void setNightModeOption(@ForceNightMode int nightModeOverride);
3839

3940
void setReportIncidentButtonEnabled(boolean enabled);
4041

android/src/main/java/com/google/android/react/navsdk/MapViewController.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.android.gms.maps.model.GroundOverlay;
2929
import com.google.android.gms.maps.model.GroundOverlayOptions;
3030
import com.google.android.gms.maps.model.LatLng;
31+
import com.google.android.gms.maps.model.MapColorScheme;
3132
import com.google.android.gms.maps.model.MapStyleOptions;
3233
import com.google.android.gms.maps.model.Marker;
3334
import com.google.android.gms.maps.model.MarkerOptions;
@@ -513,6 +514,14 @@ public void setMapType(int jsValue) {
513514
mGoogleMap.setMapType(EnumTranslationUtil.getMapTypeFromJsValue(jsValue));
514515
}
515516

517+
public void setColorScheme(@MapColorScheme int mapColorScheme) {
518+
if (mGoogleMap == null) {
519+
return;
520+
}
521+
522+
mGoogleMap.setMapColorScheme(mapColorScheme);
523+
}
524+
516525
public void clearMapView() {
517526
if (mGoogleMap == null) {
518527
return;

android/src/main/java/com/google/android/react/navsdk/MapViewFragment.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.android.gms.maps.model.Circle;
3131
import com.google.android.gms.maps.model.GroundOverlay;
3232
import com.google.android.gms.maps.model.LatLng;
33+
import com.google.android.gms.maps.model.MapColorScheme;
3334
import com.google.android.gms.maps.model.Marker;
3435
import com.google.android.gms.maps.model.Polygon;
3536
import com.google.android.gms.maps.model.Polyline;
@@ -46,6 +47,7 @@ public class MapViewFragment extends SupportMapFragment
4647
private ReactApplicationContext reactContext;
4748
private GoogleMap mGoogleMap;
4849
private MapViewController mMapViewController;
50+
private @MapColorScheme int mapColorScheme = MapColorScheme.FOLLOW_SYSTEM;
4951

5052
public static MapViewFragment newInstance(
5153
ReactApplicationContext reactContext, int viewTag, @NonNull GoogleMapOptions mapOptions) {
@@ -74,6 +76,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
7476

7577
// Setup map listeners with the provided callback
7678
mMapViewController.setupMapListeners(MapViewFragment.this);
79+
applyMapColorSchemeToMap();
7780

7881
emitEvent("onMapReady", null);
7982

@@ -137,6 +140,18 @@ public GoogleMap getGoogleMap() {
137140
return mGoogleMap;
138141
}
139142

143+
@Override
144+
public void setMapColorScheme(@MapColorScheme int mapColorScheme) {
145+
this.mapColorScheme = mapColorScheme;
146+
applyMapColorSchemeToMap();
147+
}
148+
149+
private void applyMapColorSchemeToMap() {
150+
if (mMapViewController != null) {
151+
mMapViewController.setColorScheme(mapColorScheme);
152+
}
153+
}
154+
140155
private void emitEvent(String eventName, @Nullable WritableMap data) {
141156
if (reactContext != null) {
142157
EventDispatcher dispatcher =

android/src/main/java/com/google/android/react/navsdk/NavViewFragment.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
import com.google.android.gms.maps.model.Circle;
3030
import com.google.android.gms.maps.model.GroundOverlay;
3131
import com.google.android.gms.maps.model.LatLng;
32+
import com.google.android.gms.maps.model.MapColorScheme;
3233
import com.google.android.gms.maps.model.Marker;
3334
import com.google.android.gms.maps.model.Polygon;
3435
import com.google.android.gms.maps.model.Polyline;
36+
import com.google.android.libraries.navigation.ForceNightMode;
3537
import com.google.android.libraries.navigation.NavigationView;
3638
import com.google.android.libraries.navigation.PromptVisibilityChangedListener;
3739
import com.google.android.libraries.navigation.StylingOptions;
@@ -49,6 +51,8 @@ public class NavViewFragment extends SupportNavigationFragment
4951
private MapViewController mMapViewController;
5052
private GoogleMap mGoogleMap;
5153
private StylingOptions mStylingOptions;
54+
private @MapColorScheme int mapColorScheme = MapColorScheme.FOLLOW_SYSTEM;
55+
private @ForceNightMode int nightModeOverride = ForceNightMode.AUTO;
5256

5357
public static NavViewFragment newInstance(
5458
ReactApplicationContext reactContext, int viewTag, @NonNull GoogleMapOptions mapOptions) {
@@ -79,6 +83,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
7983

8084
// Setup map listeners with the provided callback
8185
mMapViewController.setupMapListeners(NavViewFragment.this);
86+
applyMapColorSchemeToMap();
87+
applyNightModePreference();
8288

8389
emitEvent("onMapReady", null);
8490

@@ -131,8 +137,15 @@ public void setStylingOptions(StylingOptions stylingOptions) {
131137
applyStylingOptions();
132138
}
133139

134-
public void setNightModeOption(int jsValue) {
135-
super.setForceNightMode(EnumTranslationUtil.getForceNightModeFromJsValue(jsValue));
140+
public void setNightModeOption(@ForceNightMode int nightModeOverride) {
141+
this.nightModeOverride = nightModeOverride;
142+
applyNightModePreference();
143+
}
144+
145+
@Override
146+
public void setMapColorScheme(@MapColorScheme int mapColorScheme) {
147+
this.mapColorScheme = mapColorScheme;
148+
applyMapColorSchemeToMap();
136149
}
137150

138151
@Override
@@ -191,6 +204,16 @@ public GoogleMap getGoogleMap() {
191204
return mGoogleMap;
192205
}
193206

207+
private void applyMapColorSchemeToMap() {
208+
if (mMapViewController != null) {
209+
mMapViewController.setColorScheme(mapColorScheme);
210+
}
211+
}
212+
213+
private void applyNightModePreference() {
214+
super.setForceNightMode(nightModeOverride);
215+
}
216+
194217
private void cleanup() {
195218
removeOnRecenterButtonClickedListener(onRecenterButtonClickedListener);
196219
removePromptVisibilityChangedListener(onPromptVisibilityChangedListener);

android/src/main/java/com/google/android/react/navsdk/NavViewManager.java

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ public class NavViewManager extends SimpleViewManager<FrameLayout> {
4848

4949
private final HashMap<Integer, WeakReference<IMapViewFragment>> fragmentMap = new HashMap<>();
5050

51+
// Cache the latest options per view so deferred fragment creation uses fresh values.
52+
private final HashMap<Integer, ReadableMap> mapOptionsCache = new HashMap<>();
53+
5154
private ReactApplicationContext reactContext;
5255

5356
public static synchronized NavViewManager getInstance(ReactApplicationContext reactContext) {
@@ -74,21 +77,27 @@ private boolean isFragmentCreated(int viewId) {
7477

7578
/** Builds GoogleMapOptions with all configured map settings. */
7679
@NonNull
77-
private GoogleMapOptions buildGoogleMapOptions(ReadableMap mapInitializationOptions) {
80+
private GoogleMapOptions buildGoogleMapOptions(ReadableMap mapOptionsMap) {
7881
GoogleMapOptions options = new GoogleMapOptions();
79-
if (mapInitializationOptions == null) {
82+
if (mapOptionsMap == null) {
8083
return options;
8184
}
8285

83-
if (mapInitializationOptions.hasKey("mapId") && !mapInitializationOptions.isNull("mapId")) {
84-
String mapIdFromOptions = mapInitializationOptions.getString("mapId");
86+
if (mapOptionsMap.hasKey("mapId") && !mapOptionsMap.isNull("mapId")) {
87+
String mapIdFromOptions = mapOptionsMap.getString("mapId");
8588
if (mapIdFromOptions != null && !mapIdFromOptions.isEmpty()) {
8689
options.mapId(mapIdFromOptions);
8790
}
8891
}
8992

90-
if (mapInitializationOptions.hasKey("mapType") && !mapInitializationOptions.isNull("mapType")) {
91-
options.mapType(mapInitializationOptions.getInt("mapType"));
93+
if (mapOptionsMap.hasKey("mapType") && !mapOptionsMap.isNull("mapType")) {
94+
options.mapType(mapOptionsMap.getInt("mapType"));
95+
}
96+
97+
if (mapOptionsMap.hasKey("mapColorScheme")) {
98+
int jsValue =
99+
mapOptionsMap.isNull("mapColorScheme") ? 0 : mapOptionsMap.getInt("mapColorScheme");
100+
options.mapColorScheme(EnumTranslationUtil.getMapColorSchemeFromJsValue(jsValue));
92101
}
93102

94103
return options;
@@ -151,6 +160,7 @@ public void onDropViewInstance(@NonNull FrameLayout view) {
151160
if (activity == null) return;
152161

153162
WeakReference<IMapViewFragment> weakReference = fragmentMap.remove(viewId);
163+
mapOptionsCache.remove(viewId);
154164
if (weakReference != null) {
155165
IMapViewFragment fragment = weakReference.get();
156166
if (fragment != null && fragment.isAdded()) {
@@ -163,17 +173,17 @@ public void onDropViewInstance(@NonNull FrameLayout view) {
163173
}
164174
}
165175

166-
@ReactProp(name = "mapInitializationOptions")
167-
public void setMapInitializationOptions(
168-
FrameLayout view, @NonNull ReadableMap mapInitializationOptions) {
176+
@ReactProp(name = "mapOptions")
177+
public void setMapOptions(FrameLayout view, @NonNull ReadableMap mapOptions) {
169178
int viewId = view.getId();
179+
mapOptionsCache.put(viewId, mapOptions);
170180

171-
// Don't create fragment if already exists
172181
if (isFragmentCreated(viewId)) {
182+
updateMapOptionValues(viewId, mapOptions);
173183
return;
174184
}
175185

176-
scheduleFragmentTransaction(view, mapInitializationOptions);
186+
scheduleFragmentTransaction(view, mapOptions);
177187
}
178188

179189
/** Map the "create" command to an integer */
@@ -317,7 +327,8 @@ public void receiveCommand(
317327
navFragment = getNavFragmentForRoot(root);
318328
if (navFragment != null) {
319329
assert args != null;
320-
navFragment.setNightModeOption(args.getInt(0));
330+
int nightModeOverride = EnumTranslationUtil.getForceNightModeFromJsValue(args.getInt(0));
331+
navFragment.setNightModeOption(nightModeOverride);
321332
}
322333
break;
323334
case SET_SPEEDOMETER_ENABLED:
@@ -590,42 +601,81 @@ public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
590601
}
591602

592603
private void scheduleFragmentTransaction(
593-
@NonNull FrameLayout root, @NonNull ReadableMap mapInitializationOptions) {
604+
@NonNull FrameLayout root, @NonNull ReadableMap mapOptions) {
594605

595606
// Commit the fragment transaction after view is added to the view hierarchy.
596607
root.post(
597608
() -> {
598-
if (isFragmentCreated(root.getId())) {
609+
int viewId = root.getId();
610+
if (isFragmentCreated(viewId)) {
599611
return;
600612
}
601-
commitFragmentTransaction(root, mapInitializationOptions);
613+
ReadableMap latestOptions = mapOptionsCache.get(viewId);
614+
ReadableMap optionsToUse = latestOptions != null ? latestOptions : mapOptions;
615+
commitFragmentTransaction(root, optionsToUse);
602616
});
603617
}
604618

619+
private void updateMapOptionValues(int viewId, @NonNull ReadableMap mapOptions) {
620+
IMapViewFragment fragment = getFragmentForViewId(viewId);
621+
if (fragment == null) {
622+
return;
623+
}
624+
625+
if (mapOptions.hasKey("mapColorScheme")) {
626+
int jsValue = mapOptions.isNull("mapColorScheme") ? 0 : mapOptions.getInt("mapColorScheme");
627+
fragment.setMapColorScheme(EnumTranslationUtil.getMapColorSchemeFromJsValue(jsValue));
628+
}
629+
630+
if (fragment instanceof INavViewFragment
631+
&& mapOptions.hasKey("navigationStylingOptions")
632+
&& !mapOptions.isNull("navigationStylingOptions")) {
633+
ReadableMap stylingMap = mapOptions.getMap("navigationStylingOptions");
634+
if (stylingMap != null) {
635+
StylingOptions stylingOptions =
636+
new StylingOptionsBuilder.Builder(stylingMap.toHashMap()).build();
637+
((INavViewFragment) fragment).setStylingOptions(stylingOptions);
638+
}
639+
}
640+
641+
if (fragment instanceof INavViewFragment && mapOptions.hasKey("navigationNightMode")) {
642+
int nightMode =
643+
mapOptions.isNull("navigationNightMode") ? 0 : mapOptions.getInt("navigationNightMode");
644+
((INavViewFragment) fragment)
645+
.setNightModeOption(EnumTranslationUtil.getForceNightModeFromJsValue(nightMode));
646+
}
647+
}
648+
605649
/** Replace your React Native view with a custom fragment */
606650
private void commitFragmentTransaction(
607-
@NonNull FrameLayout view, @NonNull ReadableMap mapInitializationOptions) {
651+
@NonNull FrameLayout view, @NonNull ReadableMap mapOptions) {
608652

609653
FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
610654
if (activity == null) return;
611655
int viewId = view.getId();
612656
Fragment fragment;
613657

614658
CustomTypes.MapViewType mapViewType =
615-
EnumTranslationUtil.getMapViewTypeFromJsValue(
616-
mapInitializationOptions.getInt("mapViewType"));
659+
EnumTranslationUtil.getMapViewTypeFromJsValue(mapOptions.getInt("mapViewType"));
617660

618-
GoogleMapOptions googleMapOptions = buildGoogleMapOptions(mapInitializationOptions);
661+
GoogleMapOptions googleMapOptions = buildGoogleMapOptions(mapOptions);
619662

620663
if (mapViewType == CustomTypes.MapViewType.MAP) {
621664
fragment = MapViewFragment.newInstance(reactContext, viewId, googleMapOptions);
622665
} else {
623666
NavViewFragment navFragment =
624667
NavViewFragment.newInstance(reactContext, viewId, googleMapOptions);
668+
Integer nightMode = null;
669+
if (mapOptions.hasKey("navigationNightMode")) {
670+
int jsValue =
671+
mapOptions.isNull("navigationNightMode") ? 0 : mapOptions.getInt("navigationNightMode");
672+
nightMode = EnumTranslationUtil.getForceNightModeFromJsValue(jsValue);
673+
navFragment.setNightModeOption(nightMode);
674+
}
625675

626-
if (mapInitializationOptions.hasKey("navigationStylingOptions")
627-
&& !mapInitializationOptions.isNull("navigationStylingOptions")) {
628-
ReadableMap stylingOptionsMap = mapInitializationOptions.getMap("navigationStylingOptions");
676+
if (mapOptions.hasKey("navigationStylingOptions")
677+
&& !mapOptions.isNull("navigationStylingOptions")) {
678+
ReadableMap stylingOptionsMap = mapOptions.getMap("navigationStylingOptions");
629679
StylingOptions stylingOptions =
630680
new StylingOptionsBuilder.Builder(stylingOptionsMap.toHashMap()).build();
631681
navFragment.setStylingOptions(stylingOptions);

0 commit comments

Comments
 (0)