From c9b60e17c281c9f0bbf4111fa0ef971698ddbac2 Mon Sep 17 00:00:00 2001 From: gcole Date: Wed, 22 Feb 2017 17:15:28 -0800 Subject: [PATCH] Custom annotation views for Android. This gives feature parity to Custom Annotation Views between iOS and Android. - Add custom marker view and options for mapbox. - Add an implementation for the Annotation class. - Add a MarkerViewAdapater that transforms Annotations into views that get displayed on the map. - Handle adding and removing of children. - Update custom annotation docs. --- API.md | 54 +++++++- .../RNMGLAnnotationView.java | 44 +++++++ .../RNMGLAnnotationViewManager.java | 56 +++++++++ .../RNMGLCustomMarkerView.java | 18 +++ .../RNMGLCustomMarkerViewOptions.java | 88 ++++++++++++++ .../ReactNativeMapboxGLManager.java | 81 +++++++++++- .../ReactNativeMapboxGLPackage.java | 3 +- .../ReactNativeMapboxGLView.java | 115 +++++++++++++++++- 8 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationView.java create mode 100644 android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationViewManager.java create mode 100644 android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLCustomMarkerView.java create mode 100644 android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLCustomMarkerViewOptions.java diff --git a/API.md b/API.md index 5c8db17..270e6c2 100644 --- a/API.md +++ b/API.md @@ -43,7 +43,7 @@ import { MapView } from 'react-native-mapbox-gl'; | `contentInset` | `array` | Optional | Change the padding of the viewport of the map. Offset is in pixels. `[top, right, bottom, left]` `[0, 0, 0, 0]` | | `style` | React styles | Optional | Styles the actual map view container | N/A | | `debugActive` | `boolean` | Optional | Turns on debug mode. | `false` | -| `children` | `array` | Optional | An array of custom Annotation views (iOS only). You must import Annotation view from the component and put your custom React Native view inside. Annotation view must have unique id prop defined as well as coordinate | null | +| `children` | `array` | Optional | An array of custom Annotation views. See [Custom Annotations](#custom-annotations). | null | ## Callback props @@ -343,6 +343,58 @@ re-rendered. See [the example](./example.js#L116) for an illustration of this. +#### Custom Annotations + +If the default annotations do not offer enough options, you can embed react native +view directly onto the map as a custom marker view. + +The children of the `MapView` must be `Annotation` views: `import {Annotation} from 'mapbox-react-native-gl'`. +The `Annotation` view has the following required props: + +| Prop | Type | Description | +|---|---|---|---|---| +| `id`| `string` | Unique identifier for the annotation. | +| `coordinate` | ```{latitude: number, longitude: number}``` | Location of the annotation. | + +###### Known Bugs + +1. `Annotation` views do not position correctly unless they have the following style props: + `style={{alignItems: 'center', justifyContent: 'center', position: 'absolute'}}`. + +2. React Native views do not work with the regular `onAnnotationTapped` on need to + add their own tap handling (e.g. by using a `TouchableHighlight`). + +3. (*Android only*) Adding a view with style `flex: 1` and no `width` or `height` set can cause + views to behave a little strangely. It is recommended to add them. + +###### Example + +``` + + + + React View + + + + + + + + +``` + ## Mapbox Telemetry (metrics) If you hide the attribution button, you need to provide the user with a way to diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationView.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationView.java new file mode 100644 index 0000000..3a661b9 --- /dev/null +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationView.java @@ -0,0 +1,44 @@ +package com.mapbox.reactnativemapboxgl; + +import android.content.Context; +import android.util.Log; + +import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.views.view.ReactViewGroup; +import com.mapbox.mapboxsdk.geometry.LatLng; + +public class RNMGLAnnotationView extends ReactViewGroup { + + private final RNMGLAnnotationViewManager _manager; + private String annotationId; + private LatLng coordinate; + + public RNMGLAnnotationView(Context context, RNMGLAnnotationViewManager manager) { + super(context); + this._manager = manager; + } + + public LayoutParams getShadowNodeMeasurements() { + LayoutShadowNode shadowNode = _manager.getShadowNode(); + return new LayoutParams( + (int)shadowNode.getLayoutWidth(), + (int)shadowNode.getLayoutHeight() + ); + } + + public String getAnnotationId() { + return annotationId; + } + + public void setAnnotationId(String annotationId) { + this.annotationId = annotationId; + } + + public LatLng getCoordinate() { + return coordinate; + } + + public void setCoordinate(LatLng coordinate) { + this.coordinate = coordinate; + } +} diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationViewManager.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationViewManager.java new file mode 100644 index 0000000..684636b --- /dev/null +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLAnnotationViewManager.java @@ -0,0 +1,56 @@ +package com.mapbox.reactnativemapboxgl; + +import android.util.Log; + +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.annotations.ReactProp; +import com.mapbox.mapboxsdk.geometry.LatLng; + +import java.util.Map; + +/** + * Created by gcole on 2/17/17. + */ +public class RNMGLAnnotationViewManager extends ViewGroupManager { + private LayoutShadowNode _shadowNode; + + private static final String NAME = "RCTMapboxAnnotation"; + + @Override + public String getName() { + return NAME; + } + + @Override + protected RNMGLAnnotationView createViewInstance(ThemedReactContext reactContext) { + return new RNMGLAnnotationView(reactContext, this); + } + + @Override + public LayoutShadowNode createShadowNodeInstance() { + _shadowNode = super.createShadowNodeInstance(); + return _shadowNode; + } + + public LayoutShadowNode getShadowNode() { + return _shadowNode; + } + + // Props + + @ReactProp(name = "id") + public void setAnnotationId(RNMGLAnnotationView view, String value) { + view.setAnnotationId(value); + } + + @ReactProp(name = "coordinate") + public void setCoordinate(RNMGLAnnotationView view, ReadableMap map) { + LatLng coordinate = new LatLng(); + coordinate.setLatitude(map.getDouble("latitude")); + coordinate.setLongitude(map.getDouble("longitude")); + view.setCoordinate(coordinate); + } +} diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLCustomMarkerView.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLCustomMarkerView.java new file mode 100644 index 0000000..34f939a --- /dev/null +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLCustomMarkerView.java @@ -0,0 +1,18 @@ +package com.mapbox.reactnativemapboxgl; + +import com.mapbox.mapboxsdk.annotations.BaseMarkerViewOptions; +import com.mapbox.mapboxsdk.annotations.MarkerView; + +public class RNMGLCustomMarkerView extends MarkerView { + + private String annotationId; + + public RNMGLCustomMarkerView(BaseMarkerViewOptions baseMarkerViewOptions, String annotationId) { + super(baseMarkerViewOptions); + this.annotationId = annotationId; + } + + public String getAnnotationId() { + return annotationId; + } +} diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLCustomMarkerViewOptions.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLCustomMarkerViewOptions.java new file mode 100644 index 0000000..f1af897 --- /dev/null +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/RNMGLCustomMarkerViewOptions.java @@ -0,0 +1,88 @@ +package com.mapbox.reactnativemapboxgl; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +import com.mapbox.mapboxsdk.annotations.BaseMarkerViewOptions; +import com.mapbox.mapboxsdk.annotations.Icon; +import com.mapbox.mapboxsdk.annotations.IconFactory; +import com.mapbox.mapboxsdk.geometry.LatLng; + +public class RNMGLCustomMarkerViewOptions extends BaseMarkerViewOptions { + private String annotationId; + + protected RNMGLCustomMarkerViewOptions(Parcel in) { + position((LatLng) in.readParcelable(LatLng.class.getClassLoader())); + snippet(in.readString()); + title(in.readString()); + flat(in.readByte() != 0); + anchor(in.readFloat(), in.readFloat()); + infoWindowAnchor(in.readFloat(), in.readFloat()); + rotation(in.readFloat()); + visible(in.readByte() != 0); + alpha(in.readFloat()); + if (in.readByte() != 0) { + // this means we have an icon + String iconId = in.readString(); + Bitmap iconBitmap = in.readParcelable(Bitmap.class.getClassLoader()); + Icon icon = IconFactory.recreate(iconId, iconBitmap); + icon(icon); + } + annotationId(in.readString()); + } + + @Override + public RNMGLCustomMarkerViewOptions getThis() { + return this; + } + + @Override + public RNMGLCustomMarkerView getMarker() { + return new RNMGLCustomMarkerView(this, annotationId); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(getPosition(), flags); + out.writeString(getSnippet()); + out.writeString(getTitle()); + out.writeByte((byte) (isFlat() ? 1 : 0)); + out.writeFloat(getAnchorU()); + out.writeFloat(getAnchorV()); + out.writeFloat(getInfoWindowAnchorU()); + out.writeFloat(getInfoWindowAnchorV()); + out.writeFloat(getRotation()); + out.writeByte((byte) (isVisible() ? 1 : 0)); + out.writeFloat(getAlpha()); + Icon icon = getIcon(); + out.writeByte((byte) (icon != null ? 1 : 0)); + if (icon != null) { + out.writeString(getIcon().getId()); + out.writeParcelable(getIcon().getBitmap(), flags); + } + out.writeString(annotationId); + } + + public RNMGLCustomMarkerViewOptions annotationId(String annotationId) { + this.annotationId = annotationId; + return getThis(); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public RNMGLCustomMarkerViewOptions createFromParcel(Parcel in) { + return new RNMGLCustomMarkerViewOptions(in); + } + + public RNMGLCustomMarkerViewOptions[] newArray(int size) { + return new RNMGLCustomMarkerViewOptions[size]; + } + }; + +} diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLManager.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLManager.java index cbed75e..6b8dea3 100644 --- a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLManager.java +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLManager.java @@ -1,6 +1,8 @@ package com.mapbox.reactnativemapboxgl; +import android.view.View; + import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; @@ -11,9 +13,9 @@ import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.modules.core.RCTNativeAppEventEmitter; +import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.ThemedReactContext; -import com.facebook.react.uimanager.SimpleViewManager; import com.mapbox.mapboxsdk.camera.CameraPosition; import com.mapbox.mapboxsdk.camera.CameraUpdate; import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; @@ -21,20 +23,28 @@ import com.mapbox.mapboxsdk.constants.MapboxConstants; import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.geometry.LatLngBounds; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public class ReactNativeMapboxGLManager extends SimpleViewManager { +public class ReactNativeMapboxGLManager extends ViewGroupManager { private static final String REACT_CLASS = "RCTMapboxGL"; private ReactApplicationContext _context; + private List _childViews; + private Set _childListeners; public ReactNativeMapboxGLManager(ReactApplicationContext context) { super(); _context = context; + _childViews = new ArrayList<>(); + _childListeners = new HashSet<>(); } @Override @@ -46,6 +56,16 @@ public class ReactNativeMapboxGLManager extends SimpleViewManager getAnnotationViews() { + List annotationViews = new ArrayList<>(); + for (View view : _childViews) { + if (RNMGLAnnotationView.class.equals(view.getClass())) { + annotationViews.add((RNMGLAnnotationView) view); + } + } + return annotationViews; + } + // Lifecycle methods @Override @@ -82,6 +102,63 @@ public class ReactNativeMapboxGLManager extends SimpleViewManager createViewManagers(ReactApplicationContext reactContext) { return Arrays.asList( - new ReactNativeMapboxGLManager(reactContext) + new ReactNativeMapboxGLManager(reactContext), + new RNMGLAnnotationViewManager() ); } } \ No newline at end of file diff --git a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLView.java b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLView.java index 1f9e9aa..07b0de7 100644 --- a/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLView.java +++ b/android/src/main/java/com/mapbox/reactnativemapboxgl/ReactNativeMapboxGLView.java @@ -1,22 +1,40 @@ package com.mapbox.reactnativemapboxgl; import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.PointF; +import android.graphics.drawable.GradientDrawable; import android.hardware.GeomagneticField; import android.location.Location; import android.support.annotation.NonNull; import android.support.annotation.UiThread; +import android.util.Log; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Transformation; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.RelativeLayout; +import android.widget.TextView; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.facebook.react.views.view.ReactViewGroup; +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import com.mapbox.mapboxsdk.annotations.Annotation; +import com.mapbox.mapboxsdk.annotations.IconFactory; import com.mapbox.mapboxsdk.annotations.Marker; import com.mapbox.mapboxsdk.annotations.MarkerOptions; +import com.mapbox.mapboxsdk.annotations.MarkerView; +import com.mapbox.mapboxsdk.annotations.MarkerViewOptions; import com.mapbox.mapboxsdk.annotations.PolygonOptions; import com.mapbox.mapboxsdk.annotations.PolylineOptions; import com.mapbox.mapboxsdk.camera.CameraPosition; @@ -30,8 +48,14 @@ import com.mapbox.mapboxsdk.maps.MapboxMapOptions; import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; import com.mapbox.mapboxsdk.maps.UiSettings; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.UUID; import javax.annotation.Nullable; @@ -41,7 +65,7 @@ public class ReactNativeMapboxGLView extends RelativeLayout implements MapboxMap.OnMyBearingTrackingModeChangeListener, MapboxMap.OnMyLocationTrackingModeChangeListener, MapboxMap.OnMyLocationChangeListener, MapboxMap.OnMarkerClickListener, MapboxMap.OnInfoWindowClickListener, - MapView.OnMapChangedListener + MapView.OnMapChangedListener, ReactNativeMapboxGLManager.ChildListener { private MapboxMap _map = null; @@ -70,9 +94,11 @@ public class ReactNativeMapboxGLView extends RelativeLayout implements private boolean _didChangeThrottled = false; private boolean _changeWasAnimated = false; - private Map _annotations = new HashMap(); - private Map _annotationIdsToName = new HashMap(); - private Map _annotationOptions = new HashMap(); + private Map _annotations = new HashMap<>(); + private Map _annotationIdsToName = new HashMap<>(); + private Map _annotationOptions = new HashMap<>(); + private Map _customAnnodationIds = new HashMap<>(); + private Map _customAnnotationViewMap = new HashMap<>(); private android.os.Handler _handler; @@ -101,6 +127,7 @@ public class ReactNativeMapboxGLView extends RelativeLayout implements public void onDrop() { if (_mapView == null) { return; } _manager.getContext().removeLifecycleEventListener(this); + _manager.removeChildListener(this); if (!_paused) { _paused = true; _mapView.onPause(); @@ -131,7 +158,7 @@ public class ReactNativeMapboxGLView extends RelativeLayout implements private void setupMapView() { _mapOptions.camera(_initialCamera.build()); _mapView = new MapView(this.getContext(), _mapOptions); - this.addView(_mapView); + _manager.addView(this, _mapView, 0); _mapView.addOnMapChangedListener(this); _mapView.onCreate(null); _mapView.getMapAsync(this); @@ -189,7 +216,14 @@ public class ReactNativeMapboxGLView extends RelativeLayout implements _annotations.put(entry.getKey(), annotation); _annotationIdsToName.put(annotation.getId(), entry.getKey()); } + _annotationOptions.clear(); + + computeMarkerAnnotations(); + _manager.addChildListener(this); + + _map.getMarkerViewManager().addMarkerViewAdapter( + new RNMGLCustomMarkerViewAdapter(getContext())); } private void destroyMapView() { @@ -207,6 +241,52 @@ public class ReactNativeMapboxGLView extends RelativeLayout implements _mapView.onDestroy(); } + // Children + + @Override + public void childAdded(View child) { + computeMarkerAnnotations(); + } + + @Override + public void childRemoved(View child) { + computeMarkerAnnotations(); + } + + private void computeMarkerAnnotations() { + Set newAnnotationViews = new HashSet<>(_manager.getAnnotationViews()); + Set currentViews = new HashSet<>(_customAnnotationViewMap.values()); + Collection addedChildren = Sets.difference(newAnnotationViews, currentViews); + Collection removedChildren = Sets.difference(currentViews, newAnnotationViews); + + for (RNMGLAnnotationView annotationView : removedChildren) { + _customAnnotationViewMap.remove(annotationView.getAnnotationId()); + MarkerView markerView = _customAnnodationIds.remove(annotationView.getAnnotationId()); + _map.removeMarker(markerView); + } + + for (RNMGLAnnotationView annotationView : addedChildren) { + _customAnnotationViewMap.put(annotationView.getAnnotationId(), annotationView); + RNMGLCustomMarkerViewOptions options = new RNMGLCustomMarkerViewOptions() + .annotationId(annotationView.getAnnotationId()) + .position(annotationView.getCoordinate()) + .flat(true); + MarkerView markerView = _map.addMarker(options); + _customAnnodationIds.put(annotationView.getAnnotationId(), markerView); + } + + // Need a relayout to show custom marker views + _handler.post(new Runnable() { + @Override + public void run() { + _mapView.measure( + View.MeasureSpec.makeMeasureSpec(_mapView.getMeasuredWidth(), View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(_mapView.getMeasuredHeight(), View.MeasureSpec.EXACTLY)); + _mapView.layout(_mapView.getLeft(), _mapView.getTop(), _mapView.getRight(), _mapView.getBottom()); + } + }); + } + // Props public void setInitialZoomLevel(double value) { @@ -706,4 +786,29 @@ public class ReactNativeMapboxGLView extends RelativeLayout implements if (_map == null) { return; } _map.deselectMarkers(); } + + // Custom Marker View Adapter - Adapts a MarkerView to display an custom react native view. + + private class RNMGLCustomMarkerViewAdapter extends MapboxMap.MarkerViewAdapter { + + RNMGLCustomMarkerViewAdapter(@NonNull Context context) { + super(context); + } + + @Nullable + @Override + public View getView(@NonNull RNMGLCustomMarkerView marker, @Nullable View convertView, @NonNull ViewGroup parent) { + if (convertView == null) { + RNMGLAnnotationView reactView = _customAnnotationViewMap.get(marker.getAnnotationId()); + ViewGroup.LayoutParams shadowNodeMeasurements = reactView.getShadowNodeMeasurements(); + + FrameLayout layout = new FrameLayout(getContext()); + layout.setLayoutParams(shadowNodeMeasurements); + layout.addView(reactView); + + convertView = layout; + } + return convertView; + } + } }