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;
+ }
+ }
}