diff --git a/README.md b/README.md index 7d85bdc..1a4b670 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,6 @@ in an unbounded widget will cause the application to behave unexpectedly. // Permissions must have been granted by this point. { }; ``` +### Changing the NavigationView size +By default, `NavigationView` uses all the available space provided to it. To adjust the size of the NavigationView, use the `style` prop. + +```tsx + +``` + ## Contributing See the [Contributing guide](./CONTRIBUTING.md). diff --git a/android/src/main/java/com/google/android/react/navsdk/Constants.java b/android/src/main/java/com/google/android/react/navsdk/Constants.java index 93e2b6a..df28645 100644 --- a/android/src/main/java/com/google/android/react/navsdk/Constants.java +++ b/android/src/main/java/com/google/android/react/navsdk/Constants.java @@ -15,7 +15,6 @@ public class Constants { public static final String NAV_JAVASCRIPT_FLAG = "NavJavascriptBridge"; - public static final String NAV_VIEW_JAVASCRIPT_FLAG = "NavViewJavascriptBridge"; public static final String LAT_FIELD_KEY = "lat"; public static final String LNG_FIELD_KEY = "lng"; } diff --git a/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java b/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java deleted file mode 100644 index bba25e2..0000000 --- a/android/src/main/java/com/google/android/react/navsdk/INavigationViewCallback.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright 2023 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 - * - *

http://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 com.google.android.gms.maps.model.Circle; -import com.google.android.gms.maps.model.GroundOverlay; -import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.Marker; -import com.google.android.gms.maps.model.Polygon; -import com.google.android.gms.maps.model.Polyline; - -public interface INavigationViewCallback { - void onMapReady(); - - void onRecenterButtonClick(); - - void onMarkerClick(Marker marker); - - void onPolylineClick(Polyline polyline); - - void onPolygonClick(Polygon polygon); - - void onCircleClick(Circle circle); - - void onGroundOverlayClick(GroundOverlay overlay); - - void onMarkerInfoWindowTapped(Marker marker); - - void onMapClick(LatLng latLng); -} diff --git a/android/src/main/java/com/google/android/react/navsdk/NavModule.java b/android/src/main/java/com/google/android/react/navsdk/NavModule.java index f3c6dfa..bc96335 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavModule.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavModule.java @@ -187,6 +187,7 @@ private void cleanup() { mWaypoints.clear(); UiThreadUtil.runOnUiThread(() -> { + mNavigator.clearDestinations(); mNavigator.cleanup(); }); } 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 82168bd..6b1b088 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 @@ -18,14 +18,18 @@ import android.content.pm.PackageManager; import android.graphics.Color; import android.os.Bundle; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.ActivityCompat; -import androidx.fragment.app.Fragment; + +import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.UIManagerHelper; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.events.Event; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.OnMapReadyCallback; @@ -47,6 +51,7 @@ import com.google.android.libraries.navigation.NavigationView; import com.google.android.libraries.navigation.StylingOptions; import com.google.android.libraries.navigation.SupportNavigationFragment; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -59,14 +64,12 @@ import java.util.concurrent.Executors; /** - * A simple {@link Fragment} subclass. Use the {@link NavViewFragment#newInstance} factory method to - * create an instance of this fragment. + * A fragment that displays a navigation view with a Google Map using SupportNavigationFragment. + * This fragment's lifecycle is managed by NavViewManager. */ -public class NavViewFragment extends Fragment { +public class NavViewFragment extends SupportNavigationFragment { private static final String TAG = "NavViewFragment"; private GoogleMap mGoogleMap; - private SupportNavigationFragment mNavFragment; - private INavigationViewCallback navigationViewCallback; private StylingOptions mStylingOptions; private List markerList = new ArrayList<>(); @@ -74,122 +77,96 @@ public class NavViewFragment extends Fragment { private List polygonList = new ArrayList<>(); private List groundOverlayList = new ArrayList<>(); private List circleList = new ArrayList<>(); + private int viewTag; // React native view tag. + private ReactApplicationContext reactContext; + public NavViewFragment(ReactApplicationContext reactContext, int viewTag) { + this.reactContext = reactContext; + this.viewTag = viewTag; + } - private NavigationView.OnRecenterButtonClickedListener onRecenterButtonClickedListener = - new NavigationView.OnRecenterButtonClickedListener() { - @Override - public void onRecenterButtonClick() { - if (navigationViewCallback != null) navigationViewCallback.onRecenterButtonClick(); - } - }; - - + private NavigationView.OnRecenterButtonClickedListener onRecenterButtonClickedListener = new NavigationView.OnRecenterButtonClickedListener() { + @Override + public void onRecenterButtonClick() { + emitEvent("onRecenterButtonClick", null); + } + }; private String style = ""; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - @SuppressLint("MissingPermission") - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - return inflater.inflate(R.layout.fragment_nav_view, container, false); - } - @SuppressLint("MissingPermission") @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - mNavFragment = - (SupportNavigationFragment) - getChildFragmentManager().findFragmentById(R.id.navigation_fragment2); - - mNavFragment.setNavigationUiEnabled(NavModule.getInstance().getNavigator() != null); - - mNavFragment.getMapAsync( - new OnMapReadyCallback() { - public void onMapReady(GoogleMap googleMap) { - mGoogleMap = googleMap; - navigationViewCallback.onMapReady(); - - mNavFragment.setNavigationUiEnabled(NavModule.getInstance().getNavigator() != null); - - mGoogleMap.setOnMarkerClickListener( - new GoogleMap.OnMarkerClickListener() { - @Override - public boolean onMarkerClick(Marker marker) { - navigationViewCallback.onMarkerClick(marker); - return false; - } - }); - mGoogleMap.setOnPolylineClickListener( - new GoogleMap.OnPolylineClickListener() { - @Override - public void onPolylineClick(Polyline polyline) { - navigationViewCallback.onPolylineClick(polyline); - } - }); - mGoogleMap.setOnPolygonClickListener( - new GoogleMap.OnPolygonClickListener() { - @Override - public void onPolygonClick(Polygon polygon) { - navigationViewCallback.onPolygonClick(polygon); - } - }); - mGoogleMap.setOnCircleClickListener( - new GoogleMap.OnCircleClickListener() { - @Override - public void onCircleClick(Circle circle) { - navigationViewCallback.onCircleClick(circle); - } - }); - mGoogleMap.setOnGroundOverlayClickListener( - new GoogleMap.OnGroundOverlayClickListener() { - @Override - public void onGroundOverlayClick(GroundOverlay groundOverlay) { - navigationViewCallback.onGroundOverlayClick(groundOverlay); - } - }); - - mGoogleMap.setOnInfoWindowClickListener( - new GoogleMap.OnInfoWindowClickListener() { - @Override - public void onInfoWindowClick(Marker marker) { - navigationViewCallback.onMarkerInfoWindowTapped(marker); - } - }); - - mGoogleMap.setOnMapClickListener( - new GoogleMap.OnMapClickListener() { - @Override - public void onMapClick(LatLng latLng) { - navigationViewCallback.onMapClick(latLng); - } - }); + setNavigationUiEnabled(NavModule.getInstance().getNavigator() != null); + + getMapAsync(new OnMapReadyCallback() { + public void onMapReady(GoogleMap googleMap) { + mGoogleMap = googleMap; + + emitEvent("onMapReady", null); + + setNavigationUiEnabled(NavModule.getInstance().getNavigator() != null); + + mGoogleMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() { + @Override + public boolean onMarkerClick(Marker marker) { + emitEvent("onMapReady", ObjectTranslationUtil.getMapFromMarker(marker)); + return false; + } + }); + mGoogleMap.setOnPolylineClickListener(new GoogleMap.OnPolylineClickListener() { + @Override + public void onPolylineClick(Polyline polyline) { + emitEvent("onPolylineClick", ObjectTranslationUtil.getMapFromPolyline(polyline)); + } + }); + mGoogleMap.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() { + @Override + public void onPolygonClick(Polygon polygon) { + emitEvent("onPolygonClick", ObjectTranslationUtil.getMapFromPolygon(polygon)); + } + }); + mGoogleMap.setOnCircleClickListener(new GoogleMap.OnCircleClickListener() { + @Override + public void onCircleClick(Circle circle) { + emitEvent("onCircleClick", ObjectTranslationUtil.getMapFromCircle(circle)); + } + }); + mGoogleMap.setOnGroundOverlayClickListener(new GoogleMap.OnGroundOverlayClickListener() { + @Override + public void onGroundOverlayClick(GroundOverlay groundOverlay) { + emitEvent("onGroundOverlayClick", ObjectTranslationUtil.getMapFromGroundOverlay(groundOverlay)); + } + }); + + mGoogleMap.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() { + @Override + public void onInfoWindowClick(Marker marker) { + emitEvent("onInfoWindowClick", ObjectTranslationUtil.getMapFromMarker(marker)); + } + }); + + mGoogleMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() { + @Override + public void onMapClick(LatLng latLng) { + emitEvent("onMapClick", ObjectTranslationUtil.getMapFromLatLng(latLng)); } }); + } + }); - Executors.newSingleThreadExecutor() - .execute( - () -> { - requireActivity() - .runOnUiThread( - (Runnable) - () -> { - mNavFragment.addOnRecenterButtonClickedListener( - onRecenterButtonClickedListener); - }); - }); + Executors.newSingleThreadExecutor().execute(() -> { + requireActivity().runOnUiThread((Runnable) () -> { + super.addOnRecenterButtonClickedListener(onRecenterButtonClickedListener); + }); + }); } public void applyStylingOptions() { if (mStylingOptions != null) { - mNavFragment.setStylingOptions(mStylingOptions); + super.setStylingOptions(mStylingOptions); } } @@ -207,19 +184,7 @@ public void setFollowingPerspective(int jsValue) { } public void setNightModeOption(int jsValue) { - if (mNavFragment == null) { - return; - } - - mNavFragment.setForceNightMode(EnumTranslationUtil.getForceNightModeFromJsValue(jsValue)); - } - - public void setRecenterButtonEnabled(boolean isEnabled) { - if (mNavFragment == null) { - return; - } - - mNavFragment.setRecenterButtonEnabled(isEnabled); + super.setForceNightMode(EnumTranslationUtil.getForceNightModeFromJsValue(jsValue)); } public void setMapType(int jsValue) { @@ -275,8 +240,7 @@ public Circle addCircle(Map optionsMap) { CircleOptions options = new CircleOptions(); - float strokeWidth = - Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue(); + float strokeWidth = Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue(); options.strokeWidth(strokeWidth); double radius = CollectionUtil.getDouble("radius", optionsMap, 0.0); @@ -315,8 +279,7 @@ public Marker addMarker(Map optionsMap) { String title = CollectionUtil.getString("title", optionsMap); String snippet = CollectionUtil.getString("snippet", optionsMap); float alpha = Double.valueOf(CollectionUtil.getDouble("alpha", optionsMap, 1)).floatValue(); - float rotation = - Double.valueOf(CollectionUtil.getDouble("rotation", optionsMap, 0)).floatValue(); + float rotation = Double.valueOf(CollectionUtil.getDouble("rotation", optionsMap, 0)).floatValue(); boolean draggable = CollectionUtil.getBool("draggable", optionsMap, false); boolean flat = CollectionUtil.getBool("flat", optionsMap, false); boolean visible = CollectionUtil.getBool("visible", optionsMap, true); @@ -390,8 +353,7 @@ public Polygon addPolygon(Map optionsMap) { String strokeColor = CollectionUtil.getString("strokeColor", optionsMap); String fillColor = CollectionUtil.getString("fillColor", optionsMap); - float strokeWidth = - Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue(); + float strokeWidth = Double.valueOf(CollectionUtil.getDouble("strokeWidth", optionsMap, 0)).floatValue(); boolean clickable = CollectionUtil.getBool("clickable", optionsMap, false); boolean geodesic = CollectionUtil.getBool("geodesic", optionsMap, false); boolean visible = CollectionUtil.getBool("visible", optionsMap, true); @@ -442,16 +404,15 @@ public Polygon addPolygon(Map optionsMap) { } public void removeMarker(String id) { - UiThreadUtil.runOnUiThread( - () -> { - for (Marker m : markerList) { - if (m.getId().equals(id)) { - m.remove(); - markerList.remove(m); - return; - } - } - }); + UiThreadUtil.runOnUiThread(() -> { + for (Marker m : markerList) { + if (m.getId().equals(id)) { + m.remove(); + markerList.remove(m); + return; + } + } + }); } public void removePolyline(String id) { @@ -513,8 +474,7 @@ public GroundOverlay addGroundOverlay(Map map) { String imagePath = CollectionUtil.getString("imgPath", map); float width = Double.valueOf(CollectionUtil.getDouble("width", map, 0)).floatValue(); float height = Double.valueOf(CollectionUtil.getDouble("height", map, 0)).floatValue(); - float transparency = - Double.valueOf(CollectionUtil.getDouble("transparency", map, 0)).floatValue(); + float transparency = Double.valueOf(CollectionUtil.getDouble("transparency", map, 0)).floatValue(); boolean clickable = CollectionUtil.getBool("clickable", map, false); boolean visible = CollectionUtil.getBool("visible", map, true); @@ -541,22 +501,17 @@ public GroundOverlay addGroundOverlay(Map map) { } public void setMapStyle(String url) { - Executors.newSingleThreadExecutor() - .execute( - () -> { - try { - style = fetchJsonFromUrl(url); - } catch (IOException e) { - throw new RuntimeException(e); - } - requireActivity() - .runOnUiThread( - (Runnable) - () -> { - MapStyleOptions options = new MapStyleOptions(style); - mGoogleMap.setMapStyle(options); - }); - }); + Executors.newSingleThreadExecutor().execute(() -> { + try { + style = fetchJsonFromUrl(url); + } catch (IOException e) { + throw new RuntimeException(e); + } + requireActivity().runOnUiThread((Runnable) () -> { + MapStyleOptions options = new MapStyleOptions(style); + mGoogleMap.setMapStyle(options); + }); + }); } public String fetchJsonFromUrl(String urlString) throws IOException { @@ -583,7 +538,9 @@ public String fetchJsonFromUrl(String urlString) throws IOException { } - /** Moves the position of the camera to hover over Melbourne. */ + /** + * Moves the position of the camera to hover over Melbourne. + */ public void moveCamera(Map map) { LatLng latLng = ObjectTranslationUtil.getLatLngFromMap((Map) map.get("target")); @@ -591,8 +548,7 @@ public void moveCamera(Map map) { float tilt = (float) CollectionUtil.getDouble("tilt", map, 0); float bearing = (float) CollectionUtil.getDouble("bearing", map, 0); - CameraPosition cameraPosition = - CameraPosition.builder().target(latLng).zoom(zoom).tilt(tilt).bearing(bearing).build(); + CameraPosition cameraPosition = CameraPosition.builder().target(latLng).zoom(zoom).tilt(tilt).bearing(bearing).build(); mGoogleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); } @@ -674,129 +630,34 @@ public void setMyLocationEnabled(boolean isOn) { } } - public void setTrafficIncidentCards(boolean isOn) { - if (mGoogleMap != null) { - mNavFragment.setTrafficIncidentCardsEnabled(isOn); - } - } - - public void setHeaderEnabled(boolean isOn) { - if (mNavFragment == null) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mNavFragment.setHeaderEnabled(isOn); - }); - } - - public void setFooterEnabled(boolean isOn) { - if (mNavFragment == null) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mNavFragment.setEtaCardEnabled(isOn); - }); - } - - public void showRouteOverview() { - mNavFragment.showRouteOverview(); - } - public void setMapToolbarEnabled(boolean isOn) { if (mGoogleMap != null) { mGoogleMap.getUiSettings().setMapToolbarEnabled(isOn); } } - public void setNavigationViewCallback(INavigationViewCallback navigationViewCallback) { - this.navigationViewCallback = navigationViewCallback; - } - - - /** Toggles whether the location marker is enabled. */ - public void setMyLocationButtonEnabled(boolean isOn) { - if (mGoogleMap == null) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mGoogleMap.getUiSettings().setMyLocationButtonEnabled(isOn); - }); - } - - /** Toggles the visibility of the Trip Progress Bar UI. This is an EXPERIMENTAL FEATURE. */ - public void setTripProgressBarUiEnabled(boolean isOn) { - if (mNavFragment == null) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mNavFragment.setTripProgressBarEnabled(isOn); - }); - } - /** - * Toggles the visibility of speed limit icon - * - * @param isOn + * Toggles whether the location marker is enabled. */ - public void setSpeedLimitIconEnabled(boolean isOn) { - if (mNavFragment == null) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mNavFragment.setSpeedLimitIconEnabled(isOn); - }); - } - - /** Toggles whether the Navigation UI is enabled. */ - public void setNavigationUiEnabled(boolean isOn) { - if (mNavFragment == null) { - return; - } - - UiThreadUtil.runOnUiThread( - () -> { - mNavFragment.setNavigationUiEnabled(isOn); - }); - } - - public void setSpeedometerEnabled(boolean isEnable) { - if (mNavFragment == null) { + public void setMyLocationButtonEnabled(boolean isOn) { + if (mGoogleMap == null) { return; } - UiThreadUtil.runOnUiThread( - () -> { - mNavFragment.setSpeedometerEnabled(isEnable); - }); + UiThreadUtil.runOnUiThread(() -> { + mGoogleMap.getUiSettings().setMyLocationButtonEnabled(isOn); + }); } @Override public void onDestroy() { super.onDestroy(); - if (mNavFragment != null) { - mNavFragment.onDestroy(); - } cleanup(); } @Override public void onDestroyView() { super.onDestroyView(); - - if (mNavFragment != null) { - mNavFragment.onDestroyView(); - } - cleanup(); } @@ -805,7 +666,39 @@ public GoogleMap getGoogleMap() { } private void cleanup() { - mNavFragment.removeOnRecenterButtonClickedListener(onRecenterButtonClickedListener); + removeOnRecenterButtonClickedListener(onRecenterButtonClickedListener); } + private void emitEvent(String eventName, @Nullable WritableMap data) { + if (reactContext != null) { + EventDispatcher dispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, viewTag); + + if (dispatcher != null) { + int surfaceId = UIManagerHelper.getSurfaceId(reactContext); + dispatcher.dispatchEvent(new NavViewEvent(surfaceId, viewTag, eventName, data)); + } + } + } + + public class NavViewEvent extends Event { + private String eventName; + private @Nullable WritableMap eventData; + + public NavViewEvent(int surfaceId, int viewTag, String eventName, @Nullable WritableMap eventData) { + super(surfaceId, viewTag); + this.eventName = eventName; + this.eventData = eventData; + } + + @Override + public String getEventName() { + return eventName; + } + + @Override + public WritableMap getEventData() { + return eventData; + } + } } + 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 832d482..326c7ae 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 @@ -15,7 +15,6 @@ import static com.google.android.react.navsdk.Command.*; -import android.location.Location; import android.view.Choreographer; import android.view.View; import android.view.ViewGroup; @@ -25,54 +24,34 @@ import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.CatalystInstance; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.WritableArray; -import com.facebook.react.bridge.WritableMap; -import com.facebook.react.bridge.WritableNativeArray; +import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.SimpleViewManager; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.ViewGroupManager; import com.google.android.gms.maps.GoogleMap; -import com.google.android.gms.maps.model.Circle; -import com.google.android.gms.maps.model.GroundOverlay; -import com.google.android.gms.maps.model.LatLng; -import com.google.android.gms.maps.model.Marker; -import com.google.android.gms.maps.model.Polygon; -import com.google.android.gms.maps.model.Polyline; -import com.google.android.libraries.mapsplatform.turnbyturn.model.NavInfo; -import com.google.android.libraries.mapsplatform.turnbyturn.model.StepInfo; -import com.google.android.libraries.navigation.ArrivalEvent; -import com.facebook.react.uimanager.annotations.ReactPropGroup; -import com.google.android.libraries.navigation.Navigator; import java.lang.ref.WeakReference; import java.util.HashMap; import java.util.Map; import java.util.Objects; -public class NavViewManager extends ViewGroupManager implements INavigationViewCallback { +public class NavViewManager extends SimpleViewManager { public static final String REACT_CLASS = "NavViewManager"; private static NavViewManager instance; - private int propWidth = 400; - private int propHeight = 400; private final HashMap> fragmentMap = new HashMap<>(); - ReactApplicationContext reactContext; - - private NavViewManager(ReactApplicationContext reactContext) { - this.reactContext = reactContext; - } + private ReactApplicationContext reactContext; public static synchronized NavViewManager getInstance(ReactApplicationContext reactContext) { if (instance == null) { - instance = new NavViewManager(reactContext); + instance = new NavViewManager(); } + instance.setReactContext(reactContext); return instance; } @@ -82,13 +61,22 @@ public String getName() { return REACT_CLASS; } - /** Return a FrameLayout which will later hold the Fragment */ + public void setReactContext(ReactApplicationContext reactContext) { + this.reactContext = reactContext; + } + + + /** + * Return a FrameLayout which will later hold the Fragment + */ @Override public FrameLayout createViewInstance(ThemedReactContext reactContext) { return new FrameLayout(reactContext); } - /** Map the "create" command to an integer */ + /** + * Map the "create" command to an integer + */ @Nullable @Override public Map getCommandsMap() { @@ -110,9 +98,7 @@ public Map getCommandsMap() { map.put(SET_MY_LOCATION_ENABLED.toString(), SET_MY_LOCATION_ENABLED.getValue()); map.put(SET_ROTATE_GESTURES_ENABLED.toString(), SET_ROTATE_GESTURES_ENABLED.getValue()); map.put(SET_SCROLL_GESTURES_ENABLED.toString(), SET_SCROLL_GESTURES_ENABLED.getValue()); - map.put( - SET_SCROLL_GESTURES_ENABLED_DURING_ROTATE_OR_ZOOM.toString(), - SET_SCROLL_GESTURES_ENABLED_DURING_ROTATE_OR_ZOOM.getValue()); + map.put(SET_SCROLL_GESTURES_ENABLED_DURING_ROTATE_OR_ZOOM.toString(),SET_SCROLL_GESTURES_ENABLED_DURING_ROTATE_OR_ZOOM.getValue()); map.put(SET_ZOOM_CONTROLS_ENABLED.toString(), SET_ZOOM_CONTROLS_ENABLED.getValue()); map.put(SET_TILT_GESTURES_ENABLED.toString(), SET_TILT_GESTURES_ENABLED.getValue()); map.put(SET_ZOOM_GESTURES_ENABLED.toString(), SET_ZOOM_GESTURES_ENABLED.getValue()); @@ -123,12 +109,8 @@ public Map getCommandsMap() { map.put(RESET_MIN_MAX_ZOOM_LEVEL.toString(), RESET_MIN_MAX_ZOOM_LEVEL.getValue()); map.put(SET_MAP_STYLE.toString(), SET_MAP_STYLE.getValue()); map.put(ANIMATE_CAMERA.toString(), ANIMATE_CAMERA.getValue()); - map.put( - SET_TRAFFIC_INCIDENT_CARDS_ENABLED.toString(), - SET_TRAFFIC_INCIDENT_CARDS_ENABLED.getValue()); - map.put( - SET_RECENTER_BUTTON_ENABLED.toString(), - SET_RECENTER_BUTTON_ENABLED.getValue()); + map.put(SET_TRAFFIC_INCIDENT_CARDS_ENABLED.toString(), SET_TRAFFIC_INCIDENT_CARDS_ENABLED.getValue()); + map.put(SET_RECENTER_BUTTON_ENABLED.toString(), SET_RECENTER_BUTTON_ENABLED.getValue()); map.put(SHOW_ROUTE_OVERVIEW.toString(), SHOW_ROUTE_OVERVIEW.getValue()); map.put(REMOVE_MARKER.toString(), REMOVE_MARKER.getValue()); map.put(REMOVE_POLYLINE.toString(), REMOVE_POLYLINE.getValue()); @@ -140,19 +122,6 @@ public Map getCommandsMap() { return map; } - @ReactPropGroup( - names = {"width", "height"}, - customType = "Style") - public void setStyle(FrameLayout view, int index, Integer value) { - if (index == 0) { - propWidth = value; - } - - if (index == 1) { - propHeight = value; - } - } - public NavViewFragment getFragmentForRoot(ViewGroup root) { int viewId = root.getId(); return getFragmentForViewId(viewId); @@ -168,7 +137,7 @@ public NavViewFragment getFragmentForViewId(int viewId) { public NavViewFragment getAnyFragment() { if (fragmentMap.isEmpty()) { - return null; + return null; } // Return the first fragment found in the map's values collection. return fragmentMap.values().iterator().next().get(); @@ -183,16 +152,13 @@ public void applyStylingOptions() { } @Override - public void receiveCommand( - @NonNull FrameLayout root, String commandId, @Nullable ReadableArray args) { + public void receiveCommand(@NonNull FrameLayout root, String commandId, @Nullable ReadableArray args) { super.receiveCommand(root, commandId, args); int commandIdInt = Integer.parseInt(commandId); switch (Command.find(commandIdInt)) { case CREATE_FRAGMENT: - propHeight = args.getInt(0); - propWidth = args.getInt(1); - Map stylingOptions = args.getMap(2).toHashMap(); + Map stylingOptions = args.getMap(0).toHashMap(); createFragment(root, stylingOptions); break; case DELETE_FRAGMENT: @@ -204,13 +170,14 @@ public void receiveCommand( .beginTransaction() .remove(Objects.requireNonNull(fragmentMap.remove(viewId)).get()) .commitNowAllowingStateLoss(); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } break; case MOVE_CAMERA: getFragmentForRoot(root).moveCamera(args.getMap(0).toHashMap()); break; case SET_TRIP_PROGRESS_BAR_ENABLED: - getFragmentForRoot(root).setTripProgressBarUiEnabled(args.getBoolean(0)); + getFragmentForRoot(root).setTripProgressBarEnabled(args.getBoolean(0)); break; case SET_NAVIGATION_UI_ENABLED: getFragmentForRoot(root).setNavigationUiEnabled(args.getBoolean(0)); @@ -286,10 +253,10 @@ public void receiveCommand( getFragmentForRoot(root).animateCamera(args.getMap(0).toHashMap()); break; case SET_TRAFFIC_INCIDENT_CARDS_ENABLED: - getFragmentForRoot(root).setTrafficIncidentCards(args.getBoolean(0)); + getFragmentForRoot(root).setTrafficIncidentCardsEnabled(args.getBoolean(0)); break; case SET_FOOTER_ENABLED: - getFragmentForRoot(root).setFooterEnabled(args.getBoolean(0)); + getFragmentForRoot(root).setEtaCardEnabled(args.getBoolean(0)); break; case SET_HEADER_ENABLED: getFragmentForRoot(root).setHeaderEnabled(args.getBoolean(0)); @@ -318,63 +285,78 @@ public void receiveCommand( } } - /** Replace your React Native view with a custom fragment */ + @Override + public Map getExportedCustomDirectEventTypeConstants() { + Map baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants(); + Map eventTypeConstants = baseEventTypeConstants != null ? baseEventTypeConstants : new HashMap<>(); + + ((Map) eventTypeConstants).putAll(MapBuilder.builder() + .put("onRecenterButtonClick", MapBuilder.of("registrationName", "onRecenterButtonClick")) + .put("onMapReady", MapBuilder.of("registrationName", "onMapReady")) + .put("onMapClick", MapBuilder.of("registrationName", "onMapClick")) + .put("onMarkerClick", MapBuilder.of("registrationName", "onMarkerClick")) + .put("onPolylineClick", MapBuilder.of("registrationName", "onPolylineClick")) + .put("onPolygonClick", MapBuilder.of("registrationName", "onPolygonClick")) + .put("onCircleClick", MapBuilder.of("registrationName", "onCircleClick")) + .put("onGroundOverlayClick", MapBuilder.of("registrationName", "onGroundOverlayClick")) + .put("onMarkerInfoWindowTapped", MapBuilder.of("registrationName", "onMarkerInfoWindowTapped")) + .build()); + return (Map) eventTypeConstants; + } + + /** + * Replace your React Native view with a custom fragment + */ public void createFragment( - FrameLayout root, Map stylingOptions) { + FrameLayout root, Map stylingOptions) { setupLayout(root); FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity(); - int viewId = root.getId(); - NavViewFragment fragment = new NavViewFragment(); - fragmentMap.put(viewId, new WeakReference<>(fragment)); + if (activity != null) { + int viewId = root.getId(); + NavViewFragment fragment = new NavViewFragment(reactContext, root.getId()); + fragmentMap.put(viewId, new WeakReference(fragment)); - fragment.setNavigationViewCallback(this); + if (stylingOptions != null) { + fragment.setStylingOptions(stylingOptions); + } - if (stylingOptions != null) { - fragment.setStylingOptions(stylingOptions); + activity.getSupportFragmentManager() + .beginTransaction() + .replace(viewId, fragment, String.valueOf(viewId)) + .commit(); } - - activity - .getSupportFragmentManager() - .beginTransaction() - .replace(viewId, fragment, String.valueOf(viewId)) - .commit(); - } - - public void setupLayout(View view) { - Choreographer.getInstance() - .postFrameCallback( - new Choreographer.FrameCallback() { - @Override - public void doFrame(long frameTimeNanos) { - manuallyLayoutChildren(view); - view.getViewTreeObserver().dispatchOnGlobalLayout(); - Choreographer.getInstance().postFrameCallback(this); - } - }); } - /** Layout all children properly */ - public void manuallyLayoutChildren(View view) { - // propWidth and propHeight coming from react-native props - int width = propWidth; - int height = propHeight; - - view.measure( - View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)); - - view.layout(0, 0, width, height); + /** + * Set up the layout for each frame. This official RN way to do this, but a bit hacky, + * and should be changed when better solution is found. + */ + public void setupLayout(FrameLayout view) { + Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + manuallyLayoutChildren(view); + view.getViewTreeObserver().dispatchOnGlobalLayout(); + Choreographer.getInstance().postFrameCallback(this); + } + }); } - private void sendCommandToReactNative(String functionName, Object args) { - if (hasValidFragment()) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - WritableNativeArray params = new WritableNativeArray(); - if (args != null) { - params.pushString("" + args); + /** + * Layout all children properly + */ + public void manuallyLayoutChildren(FrameLayout view) { + NavViewFragment fragment = getFragmentForRoot(view); + if (fragment.isAdded()) { + View childView = fragment.getView(); + if (childView != null) { + childView.measure( + View.MeasureSpec.makeMeasureSpec(view.getMeasuredWidth(), View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(view.getMeasuredHeight(), View.MeasureSpec.EXACTLY) + ); + childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, functionName, params); } } @@ -386,96 +368,4 @@ public GoogleMap getGoogleMap(int viewId) { } } - @Override - public void onMapReady() { - sendCommandToReactNative("onMapReady", null); - } - - @Override - public void onMapClick(LatLng latLng) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(ObjectTranslationUtil.getMapFromLatLng(latLng)); - - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, "onMapClick", params); - } - - @Override - public void onRecenterButtonClick() { - sendCommandToReactNative("onRecenterButtonClick", null); - } - - @Override - public void onMarkerClick(Marker marker) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(ObjectTranslationUtil.getMapFromMarker(marker)); - - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, "onMarkerClick", params); - } - - @Override - public void onPolylineClick(Polyline polyline) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(ObjectTranslationUtil.getMapFromPolyline(polyline)); - - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, "onPolylineClick", params); - } - - @Override - public void onPolygonClick(Polygon polygon) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(ObjectTranslationUtil.getMapFromPolygon(polygon)); - - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, "onPolygonClick", params); - } - - @Override - public void onCircleClick(Circle circle) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(ObjectTranslationUtil.getMapFromCircle(circle)); - - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, "onCircleClick", params); - } - - @Override - public void onGroundOverlayClick(GroundOverlay overlay) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(ObjectTranslationUtil.getMapFromGroundOverlay(overlay)); - - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, "onGroundOverlayClick", params); - } - - @Override - public void onMarkerInfoWindowTapped(Marker marker) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - - WritableNativeArray params = new WritableNativeArray(); - params.pushMap(ObjectTranslationUtil.getMapFromMarker(marker)); - - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, "onMarkerInfoWindowTapped", params); - } - - /** - * Helper method to check if the fragment is added and the reactContext is not null. - * requireActivity throws an exception if the fragment is not added or the activity is null, - * in this case exception is caught and false is returned. - */ - private boolean hasValidFragment() { - try { - return getAnyFragment().isAdded() && getAnyFragment().requireActivity() != null && reactContext != null; - } catch (Exception e) { - return false; - } - } } diff --git a/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java b/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java index ff85255..1bda68c 100644 --- a/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java +++ b/android/src/main/java/com/google/android/react/navsdk/NavViewModule.java @@ -243,21 +243,6 @@ public void addGroundOverlay(int viewId, ReadableMap overlayOptionsMap, final Pr }); } - private void sendCommandToReactNative(String functionName, String args) { - ReactContext reactContext = getReactApplicationContext(); - - if (reactContext != null) { - CatalystInstance catalystInstance = reactContext.getCatalystInstance(); - WritableNativeArray params = new WritableNativeArray(); - - if (args != null) { - params.pushString("" + args); - } - - catalystInstance.callFunction(Constants.NAV_VIEW_JAVASCRIPT_FLAG, functionName, params); - } - } - @Override public boolean canOverrideExistingModule() { return true; diff --git a/android/src/main/java/com/google/android/react/navsdk/Package.java b/android/src/main/java/com/google/android/react/navsdk/Package.java index 7280449..6aea4c8 100644 --- a/android/src/main/java/com/google/android/react/navsdk/Package.java +++ b/android/src/main/java/com/google/android/react/navsdk/Package.java @@ -34,8 +34,9 @@ public List createViewManagers(ReactApplicationContext reactContext @Override public List createNativeModules(ReactApplicationContext reactContext) { List modules = new ArrayList<>(); - modules.add(new NavModule(reactContext, NavViewManager.getInstance(reactContext))); - modules.add(new NavViewModule(reactContext, NavViewManager.getInstance(reactContext))); + NavViewManager viewManager = NavViewManager.getInstance(reactContext); + modules.add(new NavModule(reactContext, viewManager)); + modules.add(new NavViewModule(reactContext, viewManager)); return modules; } diff --git a/example/src/MultipleMapsScreen.tsx b/example/src/MultipleMapsScreen.tsx index 3a90302..4b34dc1 100644 --- a/example/src/MultipleMapsScreen.tsx +++ b/example/src/MultipleMapsScreen.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-native/no-inline-styles */ /** * Copyright 2023 Google LLC * @@ -16,7 +15,7 @@ */ import React, { useEffect, useState, useMemo, useCallback } from 'react'; -import { Button, Dimensions, View } from 'react-native'; +import { Button, View } from 'react-native'; import Snackbar from 'react-native-snackbar'; import MapsControls from './mapsControls'; import NavigationControls from './navigationControls'; @@ -60,9 +59,11 @@ const MultipleMapsScreen = () => { useState(null); const [mapViewController2, setMapViewController2] = useState(null); - const [navigationViewController, setNavigationViewController] = + const [navigationViewController1, setNavigationViewController1] = useState(null); - + const [navigationViewController2, setNavigationViewController2] = + useState(null); + const [navigationInitialized, setNavigationInitialized] = useState(false); const { navigationController, addListeners, removeListeners } = useNavigation(); @@ -90,9 +91,23 @@ const MultipleMapsScreen = () => { showSnackbar('Traffic Updated'); }, []); - const onNavigationReady = useCallback(() => { - console.log('onNavigationReady'); - }, []); + const onNavigationReady = useCallback(async () => { + if ( + navigationViewController1 != null && + navigationViewController2 != null + ) { + await navigationViewController1.setNavigationUIEnabled(true); + await navigationViewController2.setNavigationUIEnabled(true); + console.log('onNavigationReady'); + setNavigationInitialized(true); + } + }, [navigationViewController1, navigationViewController2]); + + const onNavigationDispose = useCallback(async () => { + await navigationViewController1?.setNavigationUIEnabled(false); + await navigationViewController2?.setNavigationUIEnabled(false); + setNavigationInitialized(false); + }, [navigationViewController1, navigationViewController2]); const onNavigationInitError = useCallback( (errorCode: NavigationInitErrorCode) => { @@ -145,10 +160,7 @@ const MultipleMapsScreen = () => { const currentTimeAndDistance = await navigationController.getCurrentTimeAndDistance(); - console.log( - 'called onRemainingTimeOrDistanceChanged', - currentTimeAndDistance - ); + console.log('onRemainingTimeOrDistanceChanged', currentTimeAndDistance); }, [navigationController]); const onRouteStatusResult = useCallback( @@ -248,16 +260,6 @@ const MultipleMapsScreen = () => { setOverlayType(OverlayType.MapControls2); }, []); - const navViewWidth = useMemo(() => Dimensions.get('window').width, []); - const navViewHeight = useMemo( - () => - (Dimensions.get('window').height - - 0.05 * Dimensions.get('window').height - - 150) / - 2.0, - [] - ); - const navigationViewCallbacks: NavigationViewCallbacks = useMemo( () => ({ onRecenterButtonClick, @@ -328,60 +330,53 @@ const MultipleMapsScreen = () => { return arePermissionsApproved ? ( - - - - - - { - // pass as navigation is controller only for first view in this example. - }} - /> - - - {navigationViewController != null && navigationController != null && ( - - - - )} + + + + + {navigationViewController1 != null && + navigationController != null && + navigationInitialized && ( + + + + )} {mapViewController1 != null && ( { )} - -