com.mapbox.mapboxsdk.annotations.MarkerViewManager Maven / Gradle / Ivy
package com.mapbox.mapboxsdk.annotations;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.LongSparseArray;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.mapbox.mapboxsdk.R;
import com.mapbox.mapboxsdk.constants.MapboxConstants;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.utils.AnimatorUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Interface for interacting with ViewMarkers objects inside of a MapView.
*
* This class is responsible for managing a {@link MarkerView} item.
*
*/
public class MarkerViewManager implements MapView.OnMapChangedListener {
private final ViewGroup markerViewContainer;
private final Map markerViewMap = new HashMap<>();
private final LongSparseArray markerViewAddedListenerMap = new LongSparseArray<>();
private final List markerViewAdapters = new ArrayList<>();
// TODO refactor MapboxMap out for Projection and Transform
// Requires removing MapboxMap from Annotations by using Peer model from #6912
private MapboxMap mapboxMap;
private boolean enabled;
private long updateTime;
private MapboxMap.OnMarkerViewClickListener onMarkerViewClickListener;
private boolean isWaitingForRenderInvoke;
/**
* Creates an instance of MarkerViewManager.
*
* @param container the ViewGroup associated with the MarkerViewManager
*/
public MarkerViewManager(@NonNull ViewGroup container) {
this.markerViewContainer = container;
this.markerViewAdapters.add(new ImageMarkerViewAdapter(container.getContext()));
}
// TODO refactor MapboxMap out for Projection and Transform
// Requires removing MapboxMap from Annotations by using Peer model from #6912
public void bind(MapboxMap mapboxMap) {
this.mapboxMap = mapboxMap;
}
@Override
public void onMapChanged(@MapView.MapChange int change) {
if (isWaitingForRenderInvoke && change == MapView.DID_FINISH_RENDERING_FRAME_FULLY_RENDERED) {
isWaitingForRenderInvoke = false;
invalidateViewMarkersInVisibleRegion();
}
}
/**
* Called to enable or disable MarkerView management.
*
* @param enabled true if management should be enabled
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Called with true to wait for the next render invocation.
*
* @param waitingForRenderInvoke true if waiting for next render event
*/
public void setWaitingForRenderInvoke(boolean waitingForRenderInvoke) {
isWaitingForRenderInvoke = waitingForRenderInvoke;
}
/**
* Animate a MarkerView to a given rotation.
*
* The {@link MarkerView} will be rotated from its current rotation to the given rotation.
*
*
* @param marker the MarkerView to rotate.
* @param rotation the rotation value.
*/
public void animateRotation(@NonNull MarkerView marker, float rotation) {
View convertView = markerViewMap.get(marker);
if (convertView != null) {
AnimatorUtils.rotate(convertView, rotation);
}
}
/**
* Animate a MarkerView with a given rotation.
*
* @param marker the MarkerView to rotate by.
* @param rotation the rotation by value, limited to 0 - 360 degrees.
*/
public void animateRotationBy(@NonNull MarkerView marker, float rotation) {
View convertView = markerViewMap.get(marker);
if (convertView != null) {
convertView.animate().cancel();
// calculate new direction
float diff = rotation - convertView.getRotation();
if (diff > 180.0f) {
diff -= 360.0f;
} else if (diff < -180.0f) {
diff += 360.f;
}
AnimatorUtils.rotateBy(convertView, diff);
}
}
public void setRotation(@NonNull MarkerView marker, float rotation) {
View convertView = markerViewMap.get(marker);
if (convertView != null) {
convertView.animate().cancel();
convertView.setRotation(rotation);
}
}
/**
* Animate a MarkerView to a given alpha value.
*
* The {@link MarkerView} will be transformed from its current alpha value to the given value.
*
*
* @param marker the MarkerView to change its alpha value.
* @param alpha the alpha value.
*/
public void animateAlpha(@NonNull MarkerView marker, float alpha) {
View convertView = markerViewMap.get(marker);
if (convertView != null) {
AnimatorUtils.alpha(convertView, alpha);
}
}
/**
* Animate a MarkerVIew to be visible or invisible
*
* The {@link MarkerView} will be made {@link View#VISIBLE} or {@link View#GONE}.
*
*
* @param marker the MarkerView to change its visibility
* @param visible the flag indicating if MarkerView is visible
*/
public void animateVisible(@NonNull MarkerView marker, boolean visible) {
View convertView = markerViewMap.get(marker);
if (convertView != null) {
convertView.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
/**
* Updates the position of MarkerViews currently found in the viewport.
*
* The collection of {@link MarkerView} will be iterated and each item position will be updated.
* If an item is View state is not visible and its related flag is set to visible, the
* {@link MarkerView} will be animated to visible using alpha animation.
*
*/
public void updateMarkerViewsPosition() {
for (final MarkerView marker : markerViewMap.keySet()) {
final View convertView = markerViewMap.get(marker);
if (convertView != null) {
PointF point = mapboxMap.getProjection().toScreenLocation(marker.getPosition());
if (marker.getOffsetX() == MapboxConstants.UNMEASURED) {
// ensure view is measured first
if (marker.getWidth() == 0) {
convertView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
if (convertView.getMeasuredWidth() != 0) {
marker.setWidth(convertView.getMeasuredWidth());
marker.setHeight(convertView.getMeasuredHeight());
}
}
}
if (marker.getWidth() != 0) {
int x = (int) (marker.getAnchorU() * marker.getWidth());
int y = (int) (marker.getAnchorV() * marker.getHeight());
marker.setOffset(x, y);
}
convertView.setX(point.x - marker.getOffsetX());
convertView.setY(point.y - marker.getOffsetY());
// animate visibility
if (marker.isVisible() && convertView.getVisibility() == View.GONE) {
animateVisible(marker, true);
}
}
}
}
/**
* Set tilt on every non flat MarkerView currently shown in the Viewport.
*
* @param tilt the tilt value.
*/
public void setTilt(float tilt) {
View convertView;
for (MarkerView markerView : markerViewMap.keySet()) {
if (markerView.isFlat()) {
convertView = markerViewMap.get(markerView);
if (convertView != null) {
markerView.setTilt(tilt);
convertView.setRotationX(tilt);
}
}
}
}
/**
* Update and invalidate the MarkerView icon.
*
* @param markerView the marker view to updates.
*/
public void updateIcon(@NonNull MarkerView markerView) {
View convertView = markerViewMap.get(markerView);
if (convertView != null && convertView instanceof ImageView) {
((ImageView) convertView).setImageBitmap(markerView.getIcon().getBitmap());
}
}
/**
* Animate a MarkerView to a deselected state.
*
* The {@link com.mapbox.mapboxsdk.maps.MapboxMap.MarkerViewAdapter#onDeselect(MarkerView, View)}
* will be called to execute an animation.
*
*
* @param marker the MarkerView to deselect.
*/
public void deselect(@NonNull MarkerView marker) {
deselect(marker, true);
}
/**
* Animate a MarkerView to a deselected state.
*
* The {@link com.mapbox.mapboxsdk.maps.MapboxMap.MarkerViewAdapter#onDeselect(MarkerView, View)}
* will be called to execute an animation.
*
*
* @param marker the MarkerView to deselect.
* @param callbackToMap indicates if deselect marker must be called on MapboxMap.
*/
public void deselect(@NonNull MarkerView marker, boolean callbackToMap) {
final View convertView = markerViewMap.get(marker);
if (convertView != null) {
for (MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) {
if (adapter.getMarkerClass().equals(marker.getClass())) {
adapter.onDeselect(marker, convertView);
}
}
}
if (callbackToMap) {
mapboxMap.deselectMarker(marker);
}
marker.setSelected(false);
}
/**
* Animate a MarkerView to a selected state.
*
* @param marker the MarkerView object to select.
*/
public void select(@NonNull MarkerView marker) {
select(marker, true);
}
/**
* Animate a MarkerView to a selected state.
*
* @param marker the MarkerView object to select.
* @param callbackToMap indicates if select marker must be called on {@link MapboxMap}.
*/
public void select(@NonNull MarkerView marker, boolean callbackToMap) {
final View convertView = markerViewMap.get(marker);
for (MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) {
if (adapter.getMarkerClass().equals(marker.getClass())) {
select(marker, convertView, adapter, callbackToMap);
}
}
}
/**
* Animate a MarkerView to a selected state.
*
* The {@link com.mapbox.mapboxsdk.maps.MapboxMap.MarkerViewAdapter#onSelect(MarkerView, View, boolean)}
* will be called to execute an animation.
*
*
* @param marker the MarkerView object to select.
* @param convertView the View presentation of the MarkerView.
* @param adapter the adapter used to adapt the marker to the convertView.
*/
public void select(@NonNull MarkerView marker, View convertView, MapboxMap.MarkerViewAdapter adapter) {
select(marker, convertView, adapter, true);
}
/**
* Animate a MarkerView to a selected state.
*
* The {@link com.mapbox.mapboxsdk.maps.MapboxMap.MarkerViewAdapter#onSelect(MarkerView, View, boolean)}
* will be called to execute an animation.
*
*
* @param marker the MarkerView object to select.
* @param convertView the View presentation of the MarkerView.
* @param adapter the adapter used to adapt the marker to the convertView.
* @param callbackToMap indicates if select marker must be called on MapboxMap.
*/
public void select(@NonNull MarkerView marker, View convertView, MapboxMap.MarkerViewAdapter adapter,
boolean callbackToMap) {
if (convertView != null) {
if (adapter.onSelect(marker, convertView, false)) {
if (callbackToMap) {
mapboxMap.selectMarker(marker);
}
}
marker.setSelected(true);
convertView.bringToFront();
}
}
/**
* Get view representation from a MarkerView. If marker is not found in current viewport,
* {@code null} is returned.
*
* @param marker the marker to get the view.
* @return the Android SDK View object.
*/
@Nullable
public View getView(MarkerView marker) {
return markerViewMap.get(marker);
}
/**
* Get the view adapter for a marker.
*
* @param markerView the marker to get the view adapter.
* @return the MarkerView adapter.
*/
@Nullable
public MapboxMap.MarkerViewAdapter getViewAdapter(MarkerView markerView) {
MapboxMap.MarkerViewAdapter adapter = null;
for (MapboxMap.MarkerViewAdapter a : markerViewAdapters) {
if (a.getMarkerClass().equals(markerView.getClass())) {
adapter = a;
}
}
return adapter;
}
/**
* Remove a MarkerView from a map.
*
* The {@link MarkerView} will be removed using an alpha animation and related {@link View}
* will be released to the android.support.v4.util.Pools.SimplePool from the related
* {@link com.mapbox.mapboxsdk.maps.MapboxMap.MarkerViewAdapter}. It's possible to remove
* the {@link MarkerView} from the underlying collection if needed.
*
*
* @param marker the MarkerView to remove.
*/
public void removeMarkerView(MarkerView marker) {
final View viewHolder = markerViewMap.get(marker);
if (viewHolder != null && marker != null) {
for (final MapboxMap.MarkerViewAdapter> adapter : markerViewAdapters) {
if (adapter.getMarkerClass().equals(marker.getClass())) {
if (adapter.prepareViewForReuse(marker, viewHolder)) {
// reset offset for reuse
marker.setOffset(MapboxConstants.UNMEASURED, MapboxConstants.UNMEASURED);
adapter.releaseView(viewHolder);
}
}
}
}
marker.setMapboxMap(null);
markerViewMap.remove(marker);
}
/**
* Add a MarkerViewAdapter to the MarkerViewManager.
*
* The provided MarkerViewAdapter must supply a generic subclass of MarkerView.
*
*
* @param markerViewAdapter the MarkerViewAdapter to add.
*/
public void addMarkerViewAdapter(MapboxMap.MarkerViewAdapter markerViewAdapter) {
if (markerViewAdapter.getMarkerClass().equals(MarkerView.class)) {
throw new RuntimeException("Providing a custom MarkerViewAdapter requires subclassing MarkerView");
}
if (!markerViewAdapters.contains(markerViewAdapter)) {
markerViewAdapters.add(markerViewAdapter);
invalidateViewMarkersInVisibleRegion();
}
}
/**
* Get all MarkerViewAdapters associated with this MarkerViewManager.
*
* @return a List of MarkerViewAdapters.
*/
public List getMarkerViewAdapters() {
return markerViewAdapters;
}
/**
* Register a callback to be invoked when this view is clicked.
*
* @param listener the callback to be invoked.
*/
public void setOnMarkerViewClickListener(@Nullable MapboxMap.OnMarkerViewClickListener listener) {
onMarkerViewClickListener = listener;
}
/**
* Schedule that ViewMarkers found in the viewport are invalidated.
*
* This method is rate limited, and {@link #invalidateViewMarkersInVisibleRegion} will only be called
* once each 250 ms.
*
*/
public void update() {
if (enabled) {
long currentTime = SystemClock.elapsedRealtime();
if (currentTime < updateTime) {
updateMarkerViewsPosition();
return;
}
invalidateViewMarkersInVisibleRegion();
updateTime = currentTime + 250;
}
}
/**
* Invalidate the ViewMarkers found in the viewport.
*
* This method will remove any markers that aren't in the viewport anymore and will add new
* ones for each found Marker in the changed viewport.
*
*/
public void invalidateViewMarkersInVisibleRegion() {
RectF mapViewRect = new RectF(0, 0, markerViewContainer.getWidth(), markerViewContainer.getHeight());
List markers = mapboxMap.getMarkerViewsInRect(mapViewRect);
View convertView;
// remove old markers
Iterator iterator = markerViewMap.keySet().iterator();
while (iterator.hasNext()) {
MarkerView marker = iterator.next();
if (!markers.contains(marker)) {
// remove marker
convertView = markerViewMap.get(marker);
for (MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) {
if (adapter.getMarkerClass().equals(marker.getClass())) {
adapter.prepareViewForReuse(marker, convertView);
adapter.releaseView(convertView);
iterator.remove();
}
}
}
}
// introduce new markers
for (final MarkerView marker : markers) {
if (!markerViewMap.containsKey(marker)) {
for (final MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) {
if (adapter.getMarkerClass().equals(marker.getClass())) {
// Inflate View
convertView = (View) adapter.getViewReusePool().acquire();
final View adaptedView = adapter.getView(marker, convertView, markerViewContainer);
if (adaptedView != null) {
adaptedView.setRotationX(marker.getTilt());
adaptedView.setRotation(marker.getRotation());
adaptedView.setAlpha(marker.getAlpha());
adaptedView.setVisibility(View.GONE);
if (mapboxMap.getSelectedMarkers().contains(marker)) {
// if a marker to be shown was selected
// replay that animation with duration 0
if (adapter.onSelect(marker, adaptedView, true)) {
mapboxMap.selectMarker(marker);
}
}
adaptedView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
boolean clickHandled = false;
if (onMarkerViewClickListener != null) {
clickHandled = onMarkerViewClickListener.onMarkerClick(marker, v, adapter);
markerViewContainer.setTag(true);
}
if (!clickHandled) {
ensureInfoWindowOffset(marker);
select(marker, v, adapter);
}
}
return true;
}
});
marker.setMapboxMap(mapboxMap);
markerViewMap.put(marker, adaptedView);
if (convertView == null) {
adaptedView.setVisibility(View.GONE);
markerViewContainer.addView(adaptedView);
}
}
// notify listener is marker view is rendered
OnMarkerViewAddedListener onViewAddedListener = markerViewAddedListenerMap.get(marker.getId());
if (onViewAddedListener != null) {
onViewAddedListener.onViewAdded(marker);
markerViewAddedListenerMap.remove(marker.getId());
}
}
}
}
}
// clear map, don't keep references to MarkerView listeners that are not found in the bounds of the map.
markerViewAddedListenerMap.clear();
// trigger update to make newly added ViewMarker visible,
// these would only be updated when the map is moved.
updateMarkerViewsPosition();
}
/**
* Handles the {@link MarkerView}'s info window offset.
*
* @param marker that we are ensuring info window offset.
*/
public void ensureInfoWindowOffset(MarkerView marker) {
View view = null;
if (markerViewMap.containsKey(marker)) {
view = markerViewMap.get(marker);
} else {
for (final MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) {
if (adapter.getMarkerClass().equals(marker.getClass())) {
View convertView = (View) adapter.getViewReusePool().acquire();
view = adapter.getView(marker, convertView, markerViewContainer);
break;
}
}
}
if (view != null) {
if (marker.getWidth() == 0) {
if (view.getMeasuredWidth() == 0) {
// Ensure the marker's view is measured first
view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
}
marker.setWidth(view.getMeasuredWidth());
marker.setHeight(view.getMeasuredHeight());
}
// update position on map
if (marker.getOffsetX() == MapboxConstants.UNMEASURED) {
int x = (int) (marker.getAnchorU() * marker.getWidth());
int y = (int) (marker.getAnchorV() * marker.getHeight());
marker.setOffset(x, y);
}
// InfoWindow offset
int infoWindowOffsetX = (int) ((view.getMeasuredWidth() * marker.getInfoWindowAnchorU()) - marker.getOffsetX());
int infoWindowOffsetY = (int) ((view.getMeasuredHeight() * marker.getInfoWindowAnchorV()) - marker.getOffsetY());
marker.setTopOffsetPixels(infoWindowOffsetY);
marker.setRightOffsetPixels(infoWindowOffsetX);
}
}
public ViewGroup getMarkerViewContainer() {
return markerViewContainer;
}
public void addOnMarkerViewAddedListener(MarkerView markerView, OnMarkerViewAddedListener onMarkerViewAddedListener) {
markerViewAddedListenerMap.put(markerView.getId(), onMarkerViewAddedListener);
}
/**
* Default MarkerViewAdapter used for base class of {@link MarkerView} to adapt a MarkerView to
* an ImageView.
*/
private static class ImageMarkerViewAdapter extends MapboxMap.MarkerViewAdapter {
private LayoutInflater inflater;
ImageMarkerViewAdapter(Context context) {
super(context);
inflater = LayoutInflater.from(context);
}
@Nullable
@Override
public View getView(@NonNull MarkerView marker, @Nullable View convertView, @NonNull ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = inflater.inflate(R.layout.mapbox_view_image_marker, parent, false);
viewHolder.imageView = (ImageView) convertView.findViewById(R.id.image);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.imageView.setImageBitmap(marker.getIcon().getBitmap());
viewHolder.imageView.setContentDescription(marker.getTitle());
return convertView;
}
private static class ViewHolder {
ImageView imageView;
}
}
/**
* Interface definition invoked when the View of a MarkerView has been added to the map.
*
* {@link MapboxMap#addMarker(BaseMarkerOptions)}
* and only when the related MarkerView is found in the viewport of the map.
*
*/
public interface OnMarkerViewAddedListener {
/**
* Invoked when the View of a MarkerView has been added to the Map.
*
* @param markerView The MarkerView the View was added for
*/
void onViewAdded(@NonNull MarkerView markerView);
}
}