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.
This commit is contained in:
gcole
2017-03-28 14:53:56 -07:00
parent c0ebef23fb
commit c9b60e17c2
8 changed files with 450 additions and 9 deletions
@@ -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;
}
}
@@ -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<RNMGLAnnotationView> {
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);
}
}
@@ -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;
}
}
@@ -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<RNMGLCustomMarkerView, RNMGLCustomMarkerViewOptions> {
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<RNMGLCustomMarkerViewOptions> CREATOR
= new Parcelable.Creator<RNMGLCustomMarkerViewOptions>() {
public RNMGLCustomMarkerViewOptions createFromParcel(Parcel in) {
return new RNMGLCustomMarkerViewOptions(in);
}
public RNMGLCustomMarkerViewOptions[] newArray(int size) {
return new RNMGLCustomMarkerViewOptions[size];
}
};
}
@@ -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<ReactNativeMapboxGLView> {
public class ReactNativeMapboxGLManager extends ViewGroupManager<ReactNativeMapboxGLView> {
private static final String REACT_CLASS = "RCTMapboxGL";
private ReactApplicationContext _context;
private List<View> _childViews;
private Set<ChildListener> _childListeners;
public ReactNativeMapboxGLManager(ReactApplicationContext context) {
super();
_context = context;
_childViews = new ArrayList<>();
_childListeners = new HashSet<>();
}
@Override
@@ -46,6 +56,16 @@ public class ReactNativeMapboxGLManager extends SimpleViewManager<ReactNativeMap
return _context;
}
public List<RNMGLAnnotationView> getAnnotationViews() {
List<RNMGLAnnotationView> 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<ReactNativeMap
.build();
}
// Children
public interface ChildListener {
void childAdded(View child);
void childRemoved(View child);
}
public void addChildListener(ChildListener listener) {
_childListeners.add(listener);
}
public void removeChildListener(ChildListener listener) {
_childListeners.remove(listener);
}
@Override
public void addView(ReactNativeMapboxGLView parent, View child, int index) {
_childViews.add(index, child);
if (!RNMGLAnnotationView.class.equals(child.getClass())) {
super.addView(parent, child, getRealIndex(parent, index));
}
for (ChildListener listener : _childListeners) {
listener.childAdded(child);
}
}
@Override
public int getChildCount(ReactNativeMapboxGLView parent) {
return _childViews.size();
}
@Override
public View getChildAt(ReactNativeMapboxGLView parent, int index) {
return _childViews.get(index);
}
@Override
public void removeViewAt(ReactNativeMapboxGLView parent, int index) {
View child = _childViews.remove(index);
if (!RNMGLAnnotationView.class.equals(child.getClass())) {
super.removeViewAt(parent, getRealIndex(parent, index));
}
for (ChildListener listener : _childListeners) {
listener.childRemoved(child);
}
}
private int getRealIndex(ReactNativeMapboxGLView parent, int index) {
int annotationViews = 0;
for (int i = 0; i < index; i++) {
if (RNMGLAnnotationView.class.equals(getChildAt(parent, i).getClass())) {
annotationViews++;
}
}
return index - annotationViews;
}
// Props
@ReactProp(name = "initialZoomLevel")
@@ -29,7 +29,8 @@ public class ReactNativeMapboxGLPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new ReactNativeMapboxGLManager(reactContext)
new ReactNativeMapboxGLManager(reactContext),
new RNMGLAnnotationViewManager()
);
}
}
@@ -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<String, Annotation> _annotations = new HashMap();
private Map<Long, String> _annotationIdsToName = new HashMap();
private Map<String, RNMGLAnnotationOptions> _annotationOptions = new HashMap();
private Map<String, Annotation> _annotations = new HashMap<>();
private Map<Long, String> _annotationIdsToName = new HashMap<>();
private Map<String, RNMGLAnnotationOptions> _annotationOptions = new HashMap<>();
private Map<String, MarkerView> _customAnnodationIds = new HashMap<>();
private Map<String, RNMGLAnnotationView> _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<RNMGLAnnotationView> newAnnotationViews = new HashSet<>(_manager.getAnnotationViews());
Set<RNMGLAnnotationView> currentViews = new HashSet<>(_customAnnotationViewMap.values());
Collection<RNMGLAnnotationView> addedChildren = Sets.difference(newAnnotationViews, currentViews);
Collection<RNMGLAnnotationView> 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<RNMGLCustomMarkerView> {
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;
}
}
}