package com.mapbox.mapboxsdk.annotations; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.PointF; import android.os.CountDownTimer; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.widget.ImageView; import com.mapbox.mapboxsdk.R; import com.mapbox.mapboxsdk.maps.MapView; import com.mapbox.mapboxsdk.maps.MapboxMap; import com.mapbox.mapboxsdk.maps.Projection; 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 { private Map markerViewMap; private MapboxMap mapboxMap; private MapView mapView; private List markerViewAdapters; private long viewMarkerBoundsUpdateTime; private MapboxMap.OnMarkerViewClickListener onMarkerViewClickListener; private ImageMarkerViewAdapter defaultMarkerViewAdapter; private CountDownTimer timer; /** * Creates an instance of MarkerViewManager. * * @param mapboxMap the MapboxMap associated with the MarkerViewManager * @param mapView the MapView associated with the MarkerViewManager */ public MarkerViewManager(@NonNull MapboxMap mapboxMap, @NonNull MapView mapView) { this.mapboxMap = mapboxMap; this.markerViewAdapters = new ArrayList<>(); this.mapView = mapView; this.markerViewMap = new HashMap<>(); this.defaultMarkerViewAdapter = new ImageMarkerViewAdapter(mapView.getContext()); this.markerViewAdapters.add(defaultMarkerViewAdapter); } /** * 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 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 update() { for (final MarkerView marker : markerViewMap.keySet()) { final View convertView = markerViewMap.get(marker); if (convertView != null) { if (marker.isAnimating() || !marker.shouldAnimate()) { if (marker.isAnimating()) { // cancel ongoing animations marker.setAnimating(false); AnimatorSet set = marker.getAnimation(); List animations = set.getChildAnimations(); float x = (Float) ((ObjectAnimator) animations.get(0)).getAnimatedValue(); float y = (Float) ((ObjectAnimator) animations.get(1)).getAnimatedValue(); PointF pointF = new PointF(x + marker.getOffsetX(), y + marker.getOffsetY()); marker.setPosition(mapboxMap.getProjection().fromScreenLocation(pointF)); set.cancel(); } // update position on map PointF point = mapboxMap.getProjection().toScreenLocation(marker.getPosition()); if (marker.getOffsetX() == -1) { int x = (int) (marker.getAnchorU() * convertView.getMeasuredWidth()); int y = (int) (marker.getAnchorV() * convertView.getMeasuredHeight()); marker.setOffsetX(x); marker.setOffsetY(y); } convertView.setX(point.x - marker.getOffsetX()); convertView.setY(point.y - marker.getOffsetY()); // animate visibility if (marker.isVisible() && convertView.getVisibility() == View.GONE) { convertView.animate().cancel(); convertView.setAlpha(0); AnimatorUtils.alpha(convertView, 1); } } else { if (timer == null) { timer = new CountDownTimer(50, 560) { @Override public void onTick(long millisUntilFinished) { } @Override public void onFinish() { animate(marker, convertView); } }; } else { timer.cancel(); PointF point = mapboxMap.getProjection().toScreenLocation(marker.getPosition()); convertView.setX(point.x - marker.getOffsetX()); convertView.setY(point.y - marker.getOffsetY()); } timer.start(); } } } } /** * 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); } } } } /** * */ 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) { final View convertView = markerViewMap.get(marker); if (convertView != null) { for (MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) { if (adapter.getMarkerClass().equals(marker.getClass())) { adapter.onDeselect(marker, convertView); } } } } /** * Animate a MarkerView to a selected state. * * @param marker the MarkerView object to select */ public void select(@NonNull MarkerView marker) { final View convertView = markerViewMap.get(marker); for (MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) { if (adapter.getMarkerClass().equals(marker.getClass())) { select(marker, convertView, adapter); } } } /** * 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) { if (convertView != null) { if (adapter.onSelect(marker, convertView, false)) { mapboxMap.selectMarker(marker); } marker.setSelected(true); convertView.bringToFront(); } } /** * Get view representation from a MarkerView. *

* If marker is not found in current viewport, null is returned. *

* * @param marker the marker to get the view for * @return the android SDK View object */ @Nullable public View getView(MarkerView marker) { return markerViewMap.get(marker); } /** * 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) { boolean isAnimating = marker.isAnimating(); final View viewHolder = markerViewMap.get(marker); if (viewHolder != null && marker != null) { for (final MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) { if (adapter.getMarkerClass().equals(marker.getClass())) { if (!isAnimating) { if (adapter.prepareViewForReuse(marker, viewHolder)) { adapter.releaseView(viewHolder); } } } } } if (!isAnimating) { markerViewMap.remove(marker); } } /** * Add a MarkerViewAdapter to the MarkerViewManager. *

* The provided MarkerViewAdapter must use 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); invalidateViewMarkersInBounds(); } } /** * 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 #invalidateViewMarkersInBounds} will only be called * once each 250 ms. *

*/ public void scheduleViewMarkerInvalidation() { if (!markerViewAdapters.isEmpty()) { long currentTime = SystemClock.elapsedRealtime(); if (currentTime < viewMarkerBoundsUpdateTime) { return; } invalidateViewMarkersInBounds(); viewMarkerBoundsUpdateTime = currentTime + 250; } } /** * Invalidate the ViewMarkers found in the viewport. *

* This method will remove any markers that aren't in the viewport any more and will add new * ones for each found Marker in the changed viewport. *

*/ public void invalidateViewMarkersInBounds() { Projection projection = mapboxMap.getProjection(); List markers = mapView.getMarkerViewsInBounds(projection.getVisibleRegion().latLngBounds); View convertView; // remove old markers Iterator iterator = markerViewMap.keySet().iterator(); while (iterator.hasNext()) { MarkerView m = iterator.next(); if (!m.shouldAnimate() && !markers.contains(m)) { // remove marker convertView = markerViewMap.get(m); for (MapboxMap.MarkerViewAdapter adapter : markerViewAdapters) { if (adapter.getMarkerClass().equals(m.getClass())) { adapter.prepareViewForReuse(m, 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())) { convertView = (View) adapter.getViewReusePool().acquire(); final View adaptedView = adapter.getView(marker, convertView, mapView); if (adaptedView != null) { // tilt adaptedView.setRotationX(marker.getTilt()); // rotation adaptedView.setRotation(marker.getRotation()); // alpha adaptedView.setAlpha(marker.getAlpha()); // visible adaptedView.setVisibility(marker.isVisible() ? View.VISIBLE : 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.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { boolean clickHandled = false; if (onMarkerViewClickListener != null) { clickHandled = onMarkerViewClickListener.onMarkerClick(marker, v, adapter); } if (!clickHandled) { // InfoWindow offset int infoWindowOffsetX = (int) ((adaptedView.getWidth() * marker.getInfoWindowAnchorU()) - marker.getOffsetX()); int infoWindowOffsetY = (int) ((adaptedView.getHeight() * marker.getInfoWindowAnchorV()) - marker.getOffsetY()); marker.setTopOffsetPixels(infoWindowOffsetY); marker.setRightOffsetPixels(infoWindowOffsetX); select(marker, v, adapter); } } }); markerViewMap.put(marker, adaptedView); if (convertView == null) { mapView.getMarkerViewContainer().addView(adaptedView); } } } } } } } public void animatePosition(@NonNull MarkerView marker, long duration) { marker.setDuration(duration); marker.setStartTime(AnimationUtils.currentAnimationTimeMillis()); marker.setShouldAnimate(true); animate(marker, markerViewMap.get(marker)); } private void animate(final MarkerView marker, @Nullable View convertView) { if (convertView != null) { marker.setAnimating(true); PointF screenLocation = mapboxMap.getProjection().toScreenLocation(marker.getTargetPosition()); PointF currentLocation = mapboxMap.getProjection().toScreenLocation(marker.getPosition()); convertView.setX(currentLocation.x - marker.getOffsetX()); convertView.setY(currentLocation.y - marker.getOffsetY()); ObjectAnimator animatorX = ObjectAnimator.ofFloat(convertView, "x", screenLocation.x - marker.getOffsetX()); ObjectAnimator animatorY = ObjectAnimator.ofFloat(convertView, "y", screenLocation.y - marker.getOffsetY()); AnimatorSet set = new AnimatorSet(); set.playTogether(animatorX, animatorY); marker.setAnimation(set); set.setDuration(marker.getRemainingTime()); set.addListener(new AnimatorListenerAdapter() { private boolean canceled; @Override public void onAnimationCancel(Animator animation) { super.onAnimationCancel(animation); canceled = true; } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); if (!canceled) { marker.setShouldAnimate(false); marker.setPosition(marker.getTargetPosition()); } } }); marker.setAnimation(set); set.start(); } } /** * Default MarkerViewAdapter used for base class of MarkerView to adapt a MarkerView to an ImageView */ public static class ImageMarkerViewAdapter extends MapboxMap.MarkerViewAdapter { private LayoutInflater inflater; public 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.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()); return convertView; } private static class ViewHolder { ImageView imageView; } } }