Java tutorial
/* * Copyright 2013 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.maps.android.clustering.view; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.OvalShape; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.MessageQueue; import android.support.v4.util.Pair; import android.util.SparseArray; import android.view.ViewGroup; import android.view.animation.DecelerateInterpolator; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.Projection; import com.google.android.gms.maps.model.BitmapDescriptor; import com.google.android.gms.maps.model.BitmapDescriptorFactory; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.Marker; import com.google.android.gms.maps.model.MarkerOptions; import com.google.maps.android.R; import com.google.maps.android.clustering.Cluster; import com.google.maps.android.clustering.ClusterItem; import com.google.maps.android.clustering.ClusterManager; import com.google.maps.android.clustering.algo.Algorithm; import com.google.maps.android.geometry.Point; import com.google.maps.android.projection.SphericalMercatorProjection; import com.google.maps.android.ui.SquareTextView; import com.google.maps.android.ui.IconGenerator; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.concurrent.ConcurrentHashMap; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm.MAX_DISTANCE_AT_ZOOM; /** * The default view for a ClusterManager. Markers are animated in and out of clusters. */ public class DefaultClusterRenderer<T extends ClusterItem> implements ClusterRenderer<T> { private static final boolean SHOULD_ANIMATE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; private final GoogleMap mMap; private final IconGenerator mIconGenerator; private final ClusterManager<T> mClusterManager; private final float mDensity; private static final int[] BUCKETS = { 10, 20, 50, 100, 200, 500, 1000 }; private ShapeDrawable mColoredCircleBackground; public static final int ANIMATION_MOVE = 0; public static final int ANIMATION_FADE = 1; /** * Markers that are currently on the map. */ private HashMap<Algorithm, Set<MarkerWithPosition>> mMarkers = new HashMap<>(); /** * Icons for each bucket. */ private SparseArray<BitmapDescriptor> mIcons = new SparseArray<BitmapDescriptor>(); /** * Markers for single ClusterItems. */ private MarkerCache<T> mMarkerCache = new MarkerCache<T>(); /** * If cluster size is less than this size, display individual markers. */ private static final int MIN_CLUSTER_SIZE = 4; /** * The currently displayed set of clusters. */ private HashMap<Algorithm<T>, Set<? extends Cluster<T>>> mClusters = new HashMap<>(); /** * Lookup between markers and the associated cluster. */ private Map<Marker, Cluster<T>> mMarkerToCluster = new HashMap<Marker, Cluster<T>>(); private Map<Cluster<T>, Marker> mClusterToMarker = new HashMap<Cluster<T>, Marker>(); /** * The target zoom level for the current set of clusters. */ private float mZoom; private final ViewModifier mViewModifier = new ViewModifier(); private ClusterManager.OnClusterClickListener<T> mClickListener; private ClusterManager.OnClusterInfoWindowClickListener<T> mInfoWindowClickListener; private ClusterManager.OnClusterItemClickListener<T> mItemClickListener; private ClusterManager.OnClusterItemInfoWindowClickListener<T> mItemInfoWindowClickListener; public DefaultClusterRenderer(Context context, GoogleMap map, ClusterManager<T> clusterManager) { mMap = map; mDensity = context.getResources().getDisplayMetrics().density; mIconGenerator = new IconGenerator(context); mIconGenerator.setContentView(makeSquareTextView(context)); mIconGenerator.setTextAppearance(R.style.ClusterIcon_TextAppearance); mIconGenerator.setBackground(makeClusterBackground()); mClusterManager = clusterManager; } protected boolean shouldAnimate() { return SHOULD_ANIMATE; // return false; } @Override public void onAdd() { mClusterManager.getMarkerCollection().setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() { @Override public boolean onMarkerClick(Marker marker) { return mItemClickListener != null && mItemClickListener.onClusterItemClick(mMarkerCache.get(marker)); } }); mClusterManager.getMarkerCollection() .setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() { @Override public void onInfoWindowClick(Marker marker) { if (mItemInfoWindowClickListener != null) { mItemInfoWindowClickListener.onClusterItemInfoWindowClick(mMarkerCache.get(marker)); } } }); mClusterManager.getClusterMarkerCollection() .setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() { @Override public boolean onMarkerClick(Marker marker) { return mClickListener != null && mClickListener.onClusterClick(mMarkerToCluster.get(marker)); } }); mClusterManager.getClusterMarkerCollection() .setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() { @Override public void onInfoWindowClick(Marker marker) { if (mInfoWindowClickListener != null) { mInfoWindowClickListener.onClusterInfoWindowClick(mMarkerToCluster.get(marker)); } } }); } @Override public void onRemove() { mClusterManager.getMarkerCollection().setOnMarkerClickListener(null); mClusterManager.getClusterMarkerCollection().setOnMarkerClickListener(null); } private LayerDrawable makeClusterBackground() { mColoredCircleBackground = new ShapeDrawable(new OvalShape()); ShapeDrawable outline = new ShapeDrawable(new OvalShape()); outline.getPaint().setColor(0x80ffffff); // Transparent white. LayerDrawable background = new LayerDrawable(new Drawable[] { outline, mColoredCircleBackground }); int strokeWidth = (int) (mDensity * 3); background.setLayerInset(1, strokeWidth, strokeWidth, strokeWidth, strokeWidth); return background; } private SquareTextView makeSquareTextView(Context context) { SquareTextView squareTextView = new SquareTextView(context); ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); squareTextView.setLayoutParams(layoutParams); squareTextView.setId(R.id.text); int twelveDpi = (int) (12 * mDensity); squareTextView.setPadding(twelveDpi, twelveDpi, twelveDpi, twelveDpi); return squareTextView; } protected int getColor(int clusterSize) { final float hueRange = 220; final float sizeRange = 300; final float size = Math.min(clusterSize, sizeRange); final float hue = (sizeRange - size) * (sizeRange - size) / (sizeRange * sizeRange) * hueRange; return Color.HSVToColor(new float[] { hue, 1f, .6f }); } protected String getClusterText(int bucket) { if (bucket < BUCKETS[0]) { return String.valueOf(bucket); } return String.valueOf(bucket) + "+"; } /** * Gets the "bucket" for a particular cluster. By default, uses the number of points within the * cluster, bucketed to some set points. */ protected int getBucket(Cluster<T> cluster) { int size = cluster.getSize(); if (size <= BUCKETS[0]) { return size; } for (int i = 0; i < BUCKETS.length - 1; i++) { if (size < BUCKETS[i + 1]) { return BUCKETS[i]; } } return BUCKETS[BUCKETS.length - 1]; } /** * ViewModifier ensures only one re-rendering of the view occurs at a time, and schedules * re-rendering, which is performed by the RenderTask. */ @SuppressLint("HandlerLeak") private class ViewModifier extends Handler { private static final int RUN_TASK = 0; private static final int TASK_FINISHED = 1; private boolean mViewModificationInProgress = false; private ArrayDeque<RenderTask> mNextClusters = null; @Override public void handleMessage(Message msg) { if (msg.what == TASK_FINISHED) { mViewModificationInProgress = false; if (mNextClusters != null) { // Run the task that was queued up. sendEmptyMessage(RUN_TASK); } return; } removeMessages(RUN_TASK); if (mViewModificationInProgress) { // Busy - wait for the callback. return; } if (mNextClusters == null || mNextClusters.size() == 0) { // Nothing to do. return; } RenderTask renderTask = null; synchronized (this) { renderTask = mNextClusters.pop(); if (renderTask == null) { // Nothing to do. return; } // renderTask = mNextClusters; // mNextClusters = null; mViewModificationInProgress = true; } renderTask.setCallback(new Runnable() { @Override public void run() { sendEmptyMessage(TASK_FINISHED); } }); renderTask.setProjection(mMap.getProjection()); renderTask.setMapZoom(mMap.getCameraPosition().zoom); new Thread(renderTask).start(); } public void queue(HashSet<Pair<Algorithm<T>, Set<? extends Cluster<T>>>> sets) { synchronized (this) { // Overwrite any pending cluster tasks - we don't care about intermediate states. if (mNextClusters == null) { mNextClusters = new ArrayDeque<RenderTask>(); } mNextClusters.push(new RenderTask(sets)); // mNextClusters = new RenderTask(sets); } sendEmptyMessage(RUN_TASK); } } /** * Determine whether the cluster should be rendered as individual markers or a cluster. */ protected boolean shouldRenderAsCluster(Cluster<T> cluster) { return cluster.getSize() > MIN_CLUSTER_SIZE; } /** * Transforms the current view (represented by DefaultClusterRenderer.mClusters and DefaultClusterRenderer.mZoom) to a * new zoom level and set of clusters. * <p/> * This must be run off the UI thread. Work is coordinated in the RenderTask, then queued up to * be executed by a MarkerModifier. * <p/> * There are three stages for the render: * <p/> * 1. Markers are added to the map * <p/> * 2. Markers are animated to their final position * <p/> * 3. Any old markers are removed from the map * <p/> * When zooming in, markers are animated out from the nearest existing cluster. When zooming * out, existing clusters are animated to the nearest new cluster. */ private class RenderTask implements Runnable { HashSet<Pair<Algorithm<T>, Set<? extends Cluster<T>>>> sets; private Runnable mCallback; private Projection mProjection; private SphericalMercatorProjection mSphericalMercatorProjection; private float mMapZoom; // private Algorithm algorithm; private RenderTask(HashSet<Pair<Algorithm<T>, Set<? extends Cluster<T>>>> sets) { this.sets = sets; } /** * A callback to be run when all work has been completed. * * @param callback */ public void setCallback(Runnable callback) { mCallback = callback; } public void setProjection(Projection projection) { this.mProjection = projection; } public void setMapZoom(float zoom) { this.mMapZoom = zoom; this.mSphericalMercatorProjection = new SphericalMercatorProjection( 256 * Math.pow(2, Math.min(zoom, mZoom))); } private void runOnSet(LatLngBounds visibleBounds, Algorithm<T> algorithm, Set<? extends Cluster<T>> clusters) { Set<? extends Cluster<T>> currentCluster = DefaultClusterRenderer.this.mClusters.get(algorithm); if (clusters == currentCluster) { return; } final MarkerModifier markerModifier = new MarkerModifier(); final float zoom = mMapZoom; final boolean zoomingIn = zoom > mZoom; final float zoomDelta = zoom - mZoom; final Set<MarkerWithPosition> markersToRemove = mMarkers.get(algorithm); // TODO: Add some padding, so that markers can animate in from off-screen. // Find all of the existing clusters that are on-screen. These are candidates for // markers to animate from. List<Point> existingClustersOnScreen = null; if (DefaultClusterRenderer.this.mClusters != null && shouldAnimate()) { existingClustersOnScreen = new ArrayList<Point>(); if (currentCluster != null) { for (Cluster<T> c : currentCluster) { if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) { Point point = mSphericalMercatorProjection.toPoint(c.getPosition()); existingClustersOnScreen.add(point); } } } } final Set<MarkerWithPosition> newMarkers = Collections .newSetFromMap(new ConcurrentHashMap<MarkerWithPosition, Boolean>()); if (clusters != null) { // Create the new markers and animate them to their new positions. for (Cluster<T> c : clusters) { boolean onScreen = visibleBounds.contains(c.getPosition()); if (zoomingIn && onScreen && shouldAnimate()) { Point point = mSphericalMercatorProjection.toPoint(c.getPosition()); Point closest = findClosestCluster(existingClustersOnScreen, point); if (closest != null) { LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest); markerModifier.add(true, new CreateMarkerTask(c, algorithm, newMarkers, animateTo)); } else { markerModifier.add(true, new CreateMarkerTask(c, algorithm, newMarkers, null)); } } else { markerModifier.add(onScreen, new CreateMarkerTask(c, algorithm, newMarkers, null)); } } } // Wait for all markers to be added. markerModifier.waitUntilFree(); // Don't remove any markers that were just added. This is basically anything that had // a hit in the MarkerCache. if (markersToRemove != null) { markersToRemove.removeAll(newMarkers); } // Find all of the new clusters that were added on-screen. These are candidates for // markers to animate from. List<Point> newClustersOnScreen = null; if (shouldAnimate()) { newClustersOnScreen = new ArrayList<Point>(); if (clusters != null) { for (Cluster<T> c : clusters) { if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) { Point p = mSphericalMercatorProjection.toPoint(c.getPosition()); newClustersOnScreen.add(p); } } } } if (markersToRemove != null) { // Remove the old markers, animating them into clusters if zooming out. for (final MarkerWithPosition marker : markersToRemove) { boolean onScreen = visibleBounds.contains(marker.position); // Don't animate when zooming out more than 3 zoom levels. // TODO: drop animation based on speed of device & number of markers to animate. if (!zoomingIn && zoomDelta > -3 && onScreen && shouldAnimate()) { final Point point = mSphericalMercatorProjection.toPoint(marker.position); final Point closest = findClosestCluster(newClustersOnScreen, point); if (closest != null) { LatLng animateTo = mSphericalMercatorProjection.toLatLng(closest); markerModifier.animateThenRemove(marker, marker.position, animateTo); } else { markerModifier.remove(true, marker.marker); } } else { markerModifier.remove(onScreen, marker.marker); } } } markerModifier.waitUntilFree(); mMarkers.put(algorithm, newMarkers); if (clusters != null) { DefaultClusterRenderer.this.mClusters.put(algorithm, clusters); } else { DefaultClusterRenderer.this.mClusters.remove(algorithm); } mZoom = zoom; } @SuppressLint("NewApi") public void run() { final LatLngBounds visibleBounds = mProjection.getVisibleRegion().latLngBounds; for (Pair<Algorithm<T>, Set<? extends Cluster<T>>> pair : sets) { runOnSet(visibleBounds, pair.first, pair.second); } mCallback.run(); } } @Override public void onClustersChanged(HashSet<Pair<Algorithm<T>, Set<? extends Cluster<T>>>> sets) { mViewModifier.queue(sets); } @Override public void setOnClusterClickListener(ClusterManager.OnClusterClickListener<T> listener) { mClickListener = listener; } @Override public void setOnClusterInfoWindowClickListener(ClusterManager.OnClusterInfoWindowClickListener<T> listener) { mInfoWindowClickListener = listener; } @Override public void setOnClusterItemClickListener(ClusterManager.OnClusterItemClickListener<T> listener) { mItemClickListener = listener; } @Override public void setOnClusterItemInfoWindowClickListener( ClusterManager.OnClusterItemInfoWindowClickListener<T> listener) { mItemInfoWindowClickListener = listener; } private static double distanceSquared(Point a, Point b) { return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y); } private static Point findClosestCluster(List<Point> markers, Point point) { if (markers == null || markers.isEmpty()) return null; // TODO: make this configurable. double minDistSquared = MAX_DISTANCE_AT_ZOOM * MAX_DISTANCE_AT_ZOOM; Point closest = null; for (Point candidate : markers) { double dist = distanceSquared(candidate, point); if (dist < minDistSquared) { closest = candidate; minDistSquared = dist; } } return closest; } /** * Handles all markerWithPosition manipulations on the map. Work (such as adding, removing, or * animating a markerWithPosition) is performed while trying not to block the rest of the app's * UI. */ @SuppressLint("HandlerLeak") private class MarkerModifier extends Handler implements MessageQueue.IdleHandler { private static final int BLANK = 0; private final Lock lock = new ReentrantLock(); private final Condition busyCondition = lock.newCondition(); private Queue<CreateMarkerTask> mCreateMarkerTasks = new LinkedList<CreateMarkerTask>(); private Queue<CreateMarkerTask> mOnScreenCreateMarkerTasks = new LinkedList<CreateMarkerTask>(); private Queue<Marker> mRemoveMarkerTasks = new LinkedList<Marker>(); private Queue<Marker> mOnScreenRemoveMarkerTasks = new LinkedList<Marker>(); private Queue<AnimationTask> mAnimationTasks = new LinkedList<AnimationTask>(); /** * Whether the idle listener has been added to the UI thread's MessageQueue. */ private boolean mListenerAdded; private MarkerModifier() { super(Looper.getMainLooper()); } /** * Creates markers for a cluster some time in the future. * * @param priority whether this operation should have priority. */ public void add(boolean priority, CreateMarkerTask c) { lock.lock(); sendEmptyMessage(BLANK); if (priority) { mOnScreenCreateMarkerTasks.add(c); } else { mCreateMarkerTasks.add(c); } lock.unlock(); } /** * Removes a markerWithPosition some time in the future. * * @param priority whether this operation should have priority. * @param m the markerWithPosition to remove. */ public void remove(boolean priority, Marker m) { lock.lock(); sendEmptyMessage(BLANK); if (priority) { mOnScreenRemoveMarkerTasks.add(m); } else { mRemoveMarkerTasks.add(m); } lock.unlock(); } /** * Animates a markerWithPosition some time in the future. * * @param marker the markerWithPosition to animate. * @param from the position to animate from. * @param to the position to animate to. */ public void animate(MarkerWithPosition marker, LatLng from, LatLng to) { lock.lock(); mAnimationTasks.add(new AnimationTask(marker, from, to, false)); lock.unlock(); } /** * Animates a markerWithPosition some time in the future, and removes it when the animation * is complete. * * @param marker the markerWithPosition to animate. * @param from the position to animate from. * @param to the position to animate to. */ public void animateThenRemove(MarkerWithPosition marker, LatLng from, LatLng to) { lock.lock(); AnimationTask animationTask = new AnimationTask(marker, from, to, true); mAnimationTasks.add(animationTask); lock.unlock(); } @Override public void handleMessage(Message msg) { if (!mListenerAdded) { Looper.myQueue().addIdleHandler(this); mListenerAdded = true; } removeMessages(BLANK); lock.lock(); try { // Perform up to 10 tasks at once. // Consider only performing 10 remove tasks, not adds and animations. // Removes are relatively slow and are much better when batched. for (int i = 0; i < 10; i++) { performNextTask(); } if (!isBusy()) { mListenerAdded = false; Looper.myQueue().removeIdleHandler(this); // Signal any other threads that are waiting. busyCondition.signalAll(); } else { // Sometimes the idle queue may not be called - schedule up some work regardless // of whether the UI thread is busy or not. // TODO: try to remove this. sendEmptyMessageDelayed(BLANK, 10); } } finally { lock.unlock(); } } /** * Perform the next task. Prioritise any on-screen work. */ private void performNextTask() { if (!mOnScreenRemoveMarkerTasks.isEmpty()) { removeMarker(mOnScreenRemoveMarkerTasks.poll()); } else if (!mAnimationTasks.isEmpty()) { mAnimationTasks.poll().perform(); } else if (!mOnScreenCreateMarkerTasks.isEmpty()) { mOnScreenCreateMarkerTasks.poll().perform(this); } else if (!mCreateMarkerTasks.isEmpty()) { mCreateMarkerTasks.poll().perform(this); } else if (!mRemoveMarkerTasks.isEmpty()) { removeMarker(mRemoveMarkerTasks.poll()); } } /** * @return true if there is still work to be processed. */ public boolean isBusy() { try { lock.lock(); return !(mCreateMarkerTasks.isEmpty() && mOnScreenCreateMarkerTasks.isEmpty() && mOnScreenRemoveMarkerTasks.isEmpty() && mRemoveMarkerTasks.isEmpty() && mAnimationTasks.isEmpty()); } finally { lock.unlock(); } } /** * Blocks the calling thread until all work has been processed. */ public void waitUntilFree() { while (isBusy()) { // Sometimes the idle queue may not be called - schedule up some work regardless // of whether the UI thread is busy or not. // TODO: try to remove this. sendEmptyMessage(BLANK); lock.lock(); try { if (isBusy()) { busyCondition.await(); } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } } @Override public boolean queueIdle() { // When the UI is not busy, schedule some work. sendEmptyMessage(BLANK); return true; } } protected void removeMarker(Marker m) { m.remove(); Cluster<T> cluster = mMarkerToCluster.get(m); mClusterToMarker.remove(cluster); mMarkerCache.remove(m); mMarkerToCluster.remove(m); mClusterManager.getMarkerManager().remove(m); } /** * A cache of markers representing individual ClusterItems. */ private static class MarkerCache<T> { private Map<T, Marker> mCache = new HashMap<T, Marker>(); private Map<Marker, T> mCacheReverse = new HashMap<Marker, T>(); public Marker get(T item) { return mCache.get(item); } public T get(Marker m) { return mCacheReverse.get(m); } public void put(T item, Marker m) { mCache.put(item, m); mCacheReverse.put(m, item); } public void remove(Marker m) { T item = mCacheReverse.get(m); mCacheReverse.remove(m); mCache.remove(item); } } /** * Called before the marker for a ClusterItem is added to the map. */ protected void onBeforeClusterItemRendered(T item, MarkerOptions markerOptions) { } /** * Called before the marker for a Cluster is added to the map. * The default implementation draws a circle with a rough count of the number of items. * @param algorithm */ protected void onBeforeClusterRendered(Cluster<T> cluster, Algorithm<T> algorithm, MarkerOptions markerOptions) { int bucket = getBucket(cluster); BitmapDescriptor descriptor = mIcons.get(bucket); if (descriptor == null) { mColoredCircleBackground.getPaint().setColor(getColor(bucket)); descriptor = BitmapDescriptorFactory.fromBitmap(mIconGenerator.makeIcon(getClusterText(bucket))); mIcons.put(bucket, descriptor); } // TODO: consider adding anchor(.5, .5) (Individual markers will overlap more often) markerOptions.icon(descriptor); } /** * Called after the marker for a Cluster has been added to the map. */ protected void onClusterRendered(Cluster<T> cluster, Marker marker) { } /** * Called after the marker for a ClusterItem has been added to the map. */ protected void onClusterItemRendered(T clusterItem, Marker marker) { } /** * Get the marker from a ClusterItem * @param clusterItem ClusterItem which you will obtain its marker * @return a marker from a ClusterItem or null if it does not exists */ public Marker getMarker(T clusterItem) { return mMarkerCache.get(clusterItem); } /** * Get the ClusterItem from a marker * @param marker which you will obtain its ClusterItem * @return a ClusterItem from a marker or null if it does not exists */ public T getClusterItem(Marker marker) { return mMarkerCache.get(marker); } /** * Get the marker from a Cluster * @param cluster which you will obtain its marker * @return a marker from a cluster or null if it does not exists */ public Marker getMarker(Cluster<T> cluster) { return mClusterToMarker.get(cluster); } /** * Get the Cluster from a marker * @param marker which you will obtain its Cluster * @return a Cluster from a marker or null if it does not exists */ public Cluster<T> getCluster(Marker marker) { return mMarkerToCluster.get(marker); } protected Marker getMarkerForClusterItem(Algorithm<T> algo, T item) { return null; } /** * Creates markerWithPosition(s) for a particular cluster, animating it if necessary. */ private class CreateMarkerTask { private final Cluster<T> cluster; private final Set<MarkerWithPosition> newMarkers; private final LatLng animateFrom; private Algorithm<T> algorithm; /** * @param c the cluster to render. * @param markersAdded a collection of markers to append any created markers. * @param animateFrom the location to animate the markerWithPosition from, or null if no * animation is required. */ public CreateMarkerTask(Cluster<T> c, Algorithm<T> algo, Set<MarkerWithPosition> markersAdded, LatLng animateFrom) { this.cluster = c; this.newMarkers = markersAdded; this.animateFrom = animateFrom; this.algorithm = algo; } private void perform(MarkerModifier markerModifier) { // Don't show small clusters. Render the markers inside, instead. if (!shouldRenderAsCluster(cluster)) { for (T item : cluster.getItems()) { Marker marker = mMarkerCache.get(item); MarkerWithPosition markerWithPosition; if (marker == null) { marker = getMarkerForClusterItem(algorithm, item); if (marker == null) { MarkerOptions markerOptions = new MarkerOptions(); if (animateFrom != null) { markerOptions.position(animateFrom); } else { markerOptions.position(item.getPosition()); } onBeforeClusterItemRendered(item, markerOptions); } else { marker = mClusterManager.getMarkerCollection().addMarker(marker); if (animateFrom != null) { marker.setPosition(animateFrom); } else { marker.setPosition(item.getPosition()); } } markerWithPosition = new MarkerWithPosition(marker); mMarkerCache.put(item, marker); if (animateFrom != null) { markerModifier.animate(markerWithPosition, animateFrom, item.getPosition()); } } else { markerWithPosition = new MarkerWithPosition(marker); } onClusterItemRendered(item, marker); newMarkers.add(markerWithPosition); } return; } MarkerOptions markerOptions = new MarkerOptions() .position(animateFrom == null ? cluster.getPosition() : animateFrom); onBeforeClusterRendered(cluster, algorithm, markerOptions); Marker marker = mClusterManager.getClusterMarkerCollection().addMarker(markerOptions); mMarkerToCluster.put(marker, cluster); mClusterToMarker.put(cluster, marker); MarkerWithPosition markerWithPosition = new MarkerWithPosition(marker); if (animateFrom != null) { markerModifier.animate(markerWithPosition, animateFrom, cluster.getPosition()); } onClusterRendered(cluster, marker); newMarkers.add(markerWithPosition); } } /** * A Marker and its position. Marker.getPosition() must be called from the UI thread, so this * object allows lookup from other threads. */ private static class MarkerWithPosition { private final Marker marker; private LatLng position; private MarkerWithPosition(Marker marker) { this.marker = marker; position = marker.getPosition(); } @Override public boolean equals(Object other) { if (other instanceof MarkerWithPosition) { return marker.equals(((MarkerWithPosition) other).marker); } return false; } @Override public int hashCode() { return marker.hashCode(); } } private static final TimeInterpolator ANIMATION_INTERP = new DecelerateInterpolator(); protected int animationType = ANIMATION_MOVE; public void setAnimationType(final int type) { animationType = type; } /** * Animates a markerWithPosition from one position to another. TODO: improve performance for * slow devices (e.g. Nexus S). */ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) private class AnimationTask extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener { private final MarkerWithPosition markerWithPosition; private final Marker marker; private final LatLng from; private final LatLng to; private final boolean remove; private AnimationTask(MarkerWithPosition markerWithPosition, final LatLng from, final LatLng to, final boolean remove) { this.markerWithPosition = markerWithPosition; this.marker = markerWithPosition.marker; this.from = from; this.to = to; this.remove = remove; if (!remove) { this.marker.setAlpha(0); this.marker.setPosition(to); } } public void perform() { ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); valueAnimator.setInterpolator(ANIMATION_INTERP); valueAnimator.addUpdateListener(this); valueAnimator.addListener(this); valueAnimator.start(); } @Override public void onAnimationEnd(Animator animation) { if (remove) { removeMarker(marker); } markerWithPosition.position = to; } @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { float fraction = valueAnimator.getAnimatedFraction(); if (animationType == ANIMATION_FADE) { marker.setAlpha(remove ? (1 - fraction) : fraction); } else { double lat = (to.latitude - from.latitude) * fraction + from.latitude; double lngDelta = to.longitude - from.longitude; // Take the shortest path across the 180th meridian. if (Math.abs(lngDelta) > 180) { lngDelta -= Math.signum(lngDelta) * 360; } double lng = lngDelta * fraction + from.longitude; LatLng position = new LatLng(lat, lng); marker.setPosition(position); } } } @Override public void clearCache() { // TODO Auto-generated method stub } }