cluster) {
+ return cluster.getSize() >= mMinClusterSize;
+ }
+
+ /**
+ * Determines if the new clusters should be rendered on the map, given the old clusters. This
+ * method is primarily for optimization of performance, and the default implementation simply
+ * checks if the new clusters are equal to the old clusters, and if so, it returns false.
+ *
+ * However, there are cases where you may want to re-render the clusters even if they didn't
+ * change. For example, if you want a cluster with one item to render as a cluster above
+ * a certain zoom level and as a marker below a certain zoom level (even if the contents of the
+ * clusters themselves did not change). In this case, you could check the zoom level in an
+ * implementation of this method and if that zoom level threshold is crossed return true, else
+ * {@code return super.shouldRender(oldClusters, newClusters)}.
+ *
+ * Note that always returning true from this method could potentially have negative performance
+ * implications as clusters will be re-rendered on each pass even if they don't change.
+ *
+ * @param oldClusters The clusters from the previous iteration of the clustering algorithm
+ * @param newClusters The clusters from the current iteration of the clustering algorithm
+ * @return true if the new clusters should be rendered on the map, and false if they should not. This
+ * method is primarily for optimization of performance, and the default implementation simply
+ * checks if the new clusters are equal to the old clusters, and if so, it returns false.
+ */
+ protected boolean shouldRender(@NonNull Set extends Cluster> oldClusters, @NonNull Set extends Cluster> newClusters) {
+ return !newClusters.equals(oldClusters);
+ }
+
+ /**
+ * Transforms the current view (represented by DefaultClusterRenderer.mClusters and DefaultClusterRenderer.mZoom) to a
+ * new zoom level and set of clusters.
+ *
+ * This must be run off the UI thread. Work is coordinated in the RenderTask, then queued up to
+ * be executed by a MarkerModifier.
+ *
+ * There are three stages for the render:
+ *
+ * 1. Markers are added to the map
+ *
+ * 2. Markers are animated to their final position
+ *
+ * 3. Any old markers are removed from the map
+ *
+ * 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 {
+ final Set extends Cluster> clusters;
+ private Runnable mCallback;
+ private Projection mProjection;
+ private SphericalMercatorProjection mSphericalMercatorProjection;
+ private float mMapZoom;
+
+ private RenderTask(Set extends Cluster> clusters) {
+ this.clusters = clusters;
+ }
+
+ /**
+ * 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)));
+ }
+
+ @SuppressLint("NewApi")
+ public void run() {
+ if (!shouldRender(immutableOf(DefaultAdvancedMarkersClusterRenderer.this.mClusters), immutableOf(clusters))) {
+ mCallback.run();
+ return;
+ }
+
+ final MarkerModifier markerModifier = new MarkerModifier();
+
+ final float zoom = mMapZoom;
+ final boolean zoomingIn = zoom > mZoom;
+ final float zoomDelta = zoom - mZoom;
+
+ final Set markersToRemove = mMarkers;
+ // Prevent crashes: https://issuetracker.google.com/issues/35827242
+ LatLngBounds visibleBounds;
+ try {
+ visibleBounds = mProjection.getVisibleRegion().latLngBounds;
+ } catch (Exception e) {
+ e.printStackTrace();
+ visibleBounds = LatLngBounds.builder()
+ .include(new LatLng(0, 0))
+ .build();
+ }
+ // 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 existingClustersOnScreen = null;
+ if (DefaultAdvancedMarkersClusterRenderer.this.mClusters != null && mAnimate) {
+ existingClustersOnScreen = new ArrayList<>();
+ for (Cluster c : DefaultAdvancedMarkersClusterRenderer.this.mClusters) {
+ if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) {
+ Point point = mSphericalMercatorProjection.toPoint(c.getPosition());
+ existingClustersOnScreen.add(point);
+ }
+ }
+ }
+
+ // Create the new markers and animate them to their new positions.
+ final Set newMarkers = Collections.newSetFromMap(
+ new ConcurrentHashMap());
+ for (Cluster c : clusters) {
+ boolean onScreen = visibleBounds.contains(c.getPosition());
+ if (zoomingIn && onScreen && mAnimate) {
+ 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, newMarkers, animateTo));
+ } else {
+ markerModifier.add(true, new CreateMarkerTask(c, newMarkers, null));
+ }
+ } else {
+ markerModifier.add(onScreen, new CreateMarkerTask(c, 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.
+ markersToRemove.removeAll(newMarkers);
+
+ // Find all of the new clusters that were added on-screen. These are candidates for
+ // markers to animate from.
+ List newClustersOnScreen = null;
+ if (mAnimate) {
+ newClustersOnScreen = new ArrayList<>();
+ for (Cluster c : clusters) {
+ if (shouldRenderAsCluster(c) && visibleBounds.contains(c.getPosition())) {
+ Point p = mSphericalMercatorProjection.toPoint(c.getPosition());
+ newClustersOnScreen.add(p);
+ }
+ }
+ }
+
+ // 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 && mAnimate) {
+ 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 = newMarkers;
+ DefaultAdvancedMarkersClusterRenderer.this.mClusters = clusters;
+ mZoom = zoom;
+
+ mCallback.run();
+ }
+ }
+
+ @Override
+ public void onClustersChanged(Set extends Cluster> clusters) {
+ mViewModifier.queue(clusters);
+ }
+
+ @Override
+ public void setOnClusterClickListener(ClusterManager.OnClusterClickListener listener) {
+ mClickListener = listener;
+ }
+
+ @Override
+ public void setOnClusterInfoWindowClickListener(ClusterManager.OnClusterInfoWindowClickListener listener) {
+ mInfoWindowClickListener = listener;
+ }
+
+ @Override
+ public void setOnClusterInfoWindowLongClickListener(ClusterManager.OnClusterInfoWindowLongClickListener listener) {
+ mInfoWindowLongClickListener = listener;
+ }
+
+ @Override
+ public void setOnClusterItemClickListener(ClusterManager.OnClusterItemClickListener listener) {
+ mItemClickListener = listener;
+ }
+
+ @Override
+ public void setOnClusterItemInfoWindowClickListener(ClusterManager.OnClusterItemInfoWindowClickListener listener) {
+ mItemInfoWindowClickListener = listener;
+ }
+
+ @Override
+ public void setOnClusterItemInfoWindowLongClickListener(ClusterManager.OnClusterItemInfoWindowLongClickListener listener) {
+ mItemInfoWindowLongClickListener = listener;
+ }
+
+ @Override
+ public void setAnimation(boolean animate) {
+ mAnimate = animate;
+ }
+
+ /**
+ * {@inheritDoc} The default duration is 300 milliseconds.
+ *
+ * @param animationDurationMs long: The length of the animation, in milliseconds. This value cannot be negative.
+ */
+ @Override
+ public void setAnimationDuration(long animationDurationMs) {
+ mAnimationDurationMs = animationDurationMs;
+ }
+
+ private Set extends Cluster> immutableOf(Set extends Cluster> clusters) {
+ return clusters != null ? Collections.unmodifiableSet(clusters) : Collections.emptySet();
+ }
+
+ 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 Point findClosestCluster(List markers, Point point) {
+ if (markers == null || markers.isEmpty()) return null;
+
+ int maxDistance = mClusterManager.getAlgorithm().getMaxDistanceBetweenClusteredItems();
+ double minDistSquared = maxDistance * maxDistance;
+ 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 mCreateMarkerTasks = new LinkedList<>();
+ private Queue mOnScreenCreateMarkerTasks = new LinkedList<>();
+ private Queue mRemoveMarkerTasks = new LinkedList<>();
+ private Queue mOnScreenRemoveMarkerTasks = new LinkedList<>();
+ private Queue mAnimationTasks = new LinkedList<>();
+
+ /**
+ * 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));
+ 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);
+ animationTask.removeOnAnimationComplete(mClusterManager.getMarkerManager());
+ 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());
+ }
+ }
+
+ private void removeMarker(Marker m) {
+ mMarkerCache.remove(m);
+ mClusterMarkerCache.remove(m);
+ mClusterManager.getMarkerManager().remove(m);
+ }
+
+ /**
+ * @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;
+ }
+ }
+
+ /**
+ * A cache of markers representing individual ClusterItems.
+ */
+ private static class MarkerCache {
+ private Map mCache = new HashMap<>();
+ private Map mCacheReverse = new HashMap<>();
+
+ 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. The default implementation
+ * sets the marker and snippet text based on the respective item text if they are both
+ * available, otherwise it will set the title if available, and if not it will set the marker
+ * title to the item snippet text if that is available.
+ *
+ * The first time {@link ClusterManager#cluster()} is invoked on a set of items
+ * {@link #onBeforeClusterItemRendered(ClusterItem, AdvancedMarkerOptions)} will be called and
+ * {@link #onClusterItemUpdated(ClusterItem, Marker)} will not be called.
+ * If an item is removed and re-added (or updated) and {@link ClusterManager#cluster()} is
+ * invoked again, then {@link #onClusterItemUpdated(ClusterItem, Marker)} will be called and
+ * {@link #onBeforeClusterItemRendered(ClusterItem, AdvancedMarkerOptions)} will not be called.
+ *
+ * @param item item to be rendered
+ * @param advancedMarkerOptions the AdvancedMarkerOptions representing the provided item
+ */
+ protected void onBeforeClusterItemRendered(@NonNull T item, @NonNull AdvancedMarkerOptions advancedMarkerOptions) {
+ if (item.getTitle() != null && item.getSnippet() != null) {
+ advancedMarkerOptions.title(item.getTitle());
+ advancedMarkerOptions.snippet(item.getSnippet());
+ } else if (item.getTitle() != null) {
+ advancedMarkerOptions.title(item.getTitle());
+ } else if (item.getSnippet() != null) {
+ advancedMarkerOptions.title(item.getSnippet());
+ }
+ }
+
+ /**
+ * Called when a cached marker for a ClusterItem already exists on the map so the marker may
+ * be updated to the latest item values. Default implementation updates the title and snippet
+ * of the marker if they have changed and refreshes the info window of the marker if it is open.
+ * Note that the contents of the item may not have changed since the cached marker was created -
+ * implementations of this method are responsible for checking if something changed (if that
+ * matters to the implementation).
+ *
+ * The first time {@link ClusterManager#cluster()} is invoked on a set of items
+ * {@link #onBeforeClusterItemRendered(ClusterItem, AdvancedMarkerOptions)} will be called and
+ * {@link #onClusterItemUpdated(ClusterItem, Marker)} will not be called.
+ * If an item is removed and re-added (or updated) and {@link ClusterManager#cluster()} is
+ * invoked again, then {@link #onClusterItemUpdated(ClusterItem, Marker)} will be called and
+ * {@link #onBeforeClusterItemRendered(ClusterItem, AdvancedMarkerOptions)} will not be called.
+ *
+ * @param item item being updated
+ * @param marker cached marker that contains a potentially previous state of the item.
+ */
+ protected void onClusterItemUpdated(@NonNull T item, @NonNull Marker marker) {
+ boolean changed = false;
+ // Update marker text if the item text changed - same logic as adding marker in CreateMarkerTask.perform()
+ if (item.getTitle() != null && item.getSnippet() != null) {
+ if (!item.getTitle().equals(marker.getTitle())) {
+ marker.setTitle(item.getTitle());
+ changed = true;
+ }
+ if (!item.getSnippet().equals(marker.getSnippet())) {
+ marker.setSnippet(item.getSnippet());
+ changed = true;
+ }
+ } else if (item.getSnippet() != null && !item.getSnippet().equals(marker.getTitle())) {
+ marker.setTitle(item.getSnippet());
+ changed = true;
+ } else if (item.getTitle() != null && !item.getTitle().equals(marker.getTitle())) {
+ marker.setTitle(item.getTitle());
+ changed = true;
+ }
+ // Update marker position if the item changed position
+ if (!marker.getPosition().equals(item.getPosition())) {
+ marker.setPosition(item.getPosition());
+ if (item.getZIndex() != null) {
+ marker.setZIndex(item.getZIndex());
+ }
+ changed = true;
+ }
+ if (changed && marker.isInfoWindowShown()) {
+ // Force a refresh of marker info window contents
+ marker.showInfoWindow();
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * The first time {@link ClusterManager#cluster()} is invoked on a set of items
+ * {@link #onBeforeClusterRendered(Cluster, AdvancedMarkerOptions)} will be called and
+ * {@link #onClusterUpdated(Cluster, AdvancedMarker)} will not be called. If an item is removed and
+ * re-added (or updated) and {@link ClusterManager#cluster()} is invoked
+ * again, then {@link #onClusterUpdated(Cluster, AdvancedMarker)} will be called and
+ * {@link #onBeforeClusterRendered(Cluster, AdvancedMarkerOptions)} will not be called.
+ *
+ * @param cluster cluster to be rendered
+ * @param advancedMarkerOptions markerOptions representing the provided cluster
+ */
+ protected void onBeforeClusterRendered(@NonNull Cluster cluster, @NonNull AdvancedMarkerOptions advancedMarkerOptions) {
+ // TODO: consider adding anchor(.5, .5) (Individual markers will overlap more often)
+ advancedMarkerOptions.icon(getDescriptorForCluster(cluster));
+ }
+
+ /**
+ * Gets a BitmapDescriptor for the given cluster that contains a rough count of the number of
+ * items. Used to set the cluster marker icon in the default implementations of
+ * {@link #onBeforeClusterRendered(Cluster, AdvancedMarkerOptions)} and
+ * {@link #onClusterUpdated(Cluster, AdvancedMarker)}.
+ *
+ * @param cluster cluster to get BitmapDescriptor for
+ * @return a BitmapDescriptor for the marker icon for the given cluster that contains a rough
+ * count of the number of items.
+ */
+ @NonNull
+ protected BitmapDescriptor getDescriptorForCluster(@NonNull Cluster cluster) {
+ int bucket = getBucket(cluster);
+ BitmapDescriptor descriptor = mIcons.get(bucket);
+ if (descriptor == null) {
+ mColoredCircleBackground.getPaint().setColor(getColor(bucket));
+ mIconGenerator.setTextAppearance(getClusterTextAppearance(bucket));
+ descriptor = BitmapDescriptorFactory.fromBitmap(mIconGenerator.makeIcon(getClusterText(bucket)));
+ mIcons.put(bucket, descriptor);
+ }
+ return descriptor;
+ }
+
+ /**
+ * Called after the marker for a Cluster has been added to the map.
+ *
+ * @param cluster the cluster that was just added to the map
+ * @param marker the marker representing the cluster that was just added to the map
+ */
+ protected void onClusterRendered(@NonNull Cluster cluster, @NonNull Marker marker) {
+ }
+
+ /**
+ * Called when a cached marker for a Cluster already exists on the map so the marker may
+ * be updated to the latest cluster values. Default implementation updated the icon with a
+ * circle with a rough count of the number of items. Note that the contents of the cluster may
+ * not have changed since the cached marker was created - implementations of this method are
+ * responsible for checking if something changed (if that matters to the implementation).
+ *
+ * The first time {@link ClusterManager#cluster()} is invoked on a set of items
+ * {@link #onBeforeClusterRendered(Cluster, AdvancedMarkerOptions)} will be called and
+ * {@link #onClusterUpdated(Cluster, AdvancedMarker)} will not be called. If an item is removed and
+ * re-added (or updated) and {@link ClusterManager#cluster()} is invoked
+ * again, then {@link #onClusterUpdated(Cluster, AdvancedMarker)} will be called and
+ * {@link #onBeforeClusterRendered(Cluster, AdvancedMarkerOptions)} will not be called.
+ *
+ * @param cluster cluster being updated
+ * @param marker cached marker that contains a potentially previous state of the cluster
+ */
+ protected void onClusterUpdated(@NonNull Cluster cluster, @NonNull AdvancedMarker marker) {
+ // TODO: consider adding anchor(.5, .5) (Individual markers will overlap more often)
+ marker.setIcon(getDescriptorForCluster(cluster));
+ }
+
+ /**
+ * Called after the marker for a ClusterItem has been added to the map.
+ *
+ * @param clusterItem the item that was just added to the map
+ * @param marker the marker representing the item that was just added to the map
+ */
+ protected void onClusterItemRendered(@NonNull T clusterItem, @NonNull 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 cluster) {
+ return mClusterMarkerCache.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 getCluster(Marker marker) {
+ return mClusterMarkerCache.get(marker);
+ }
+
+ /**
+ * Creates markerWithPosition(s) for a particular cluster, animating it if necessary.
+ */
+ private class CreateMarkerTask {
+ private final Cluster cluster;
+ private final Set newMarkers;
+ private final LatLng animateFrom;
+
+ /**
+ * @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 c, Set markersAdded, LatLng animateFrom) {
+ this.cluster = c;
+ this.newMarkers = markersAdded;
+ this.animateFrom = animateFrom;
+ }
+
+ private void perform(MarkerModifier markerModifier) {
+ // Don't show small clusters. Render the markers inside, instead.
+ if (!shouldRenderAsCluster(cluster)) {
+ for (T item : cluster.getItems()) {
+ AdvancedMarker marker = (AdvancedMarker)mMarkerCache.get(item);
+ MarkerWithPosition markerWithPosition;
+ if (marker == null) {
+ AdvancedMarkerOptions advancedMarkerOptions = new AdvancedMarkerOptions();
+ if (animateFrom != null) {
+ advancedMarkerOptions.position(animateFrom);
+ } else {
+ advancedMarkerOptions.position(item.getPosition());
+ if (item.getZIndex() != null) {
+ advancedMarkerOptions.zIndex(item.getZIndex());
+ }
+ }
+ onBeforeClusterItemRendered(item, advancedMarkerOptions);
+ marker = (AdvancedMarker)mClusterManager.getMarkerCollection().addMarker(advancedMarkerOptions);
+ markerWithPosition = new MarkerWithPosition(marker);
+ mMarkerCache.put(item, marker);
+ if (animateFrom != null) {
+ markerModifier.animate(markerWithPosition, animateFrom, item.getPosition());
+ }
+ } else {
+ markerWithPosition = new MarkerWithPosition(marker);
+ onClusterItemUpdated(item, marker);
+ }
+ onClusterItemRendered(item, marker);
+ newMarkers.add(markerWithPosition);
+ }
+ return;
+ }
+
+ AdvancedMarker marker = (AdvancedMarker)mClusterMarkerCache.get(cluster);
+ MarkerWithPosition markerWithPosition;
+ if (marker == null) {
+ AdvancedMarkerOptions advancedMarkerOptions = new AdvancedMarkerOptions().
+ position(animateFrom == null ? cluster.getPosition() : animateFrom);
+ onBeforeClusterRendered(cluster, advancedMarkerOptions);
+ Object object = mClusterManager.getClusterMarkerCollection().addMarker(advancedMarkerOptions);
+ marker = (AdvancedMarker) object;
+ mClusterMarkerCache.put(cluster, marker);
+ markerWithPosition = new MarkerWithPosition(marker);
+ if (animateFrom != null) {
+ markerModifier.animate(markerWithPosition, animateFrom, cluster.getPosition());
+ }
+ } else {
+ markerWithPosition = new MarkerWithPosition(marker);
+ onClusterUpdated(cluster, marker);
+ }
+ onClusterRendered(cluster, marker);
+ newMarkers.add(markerWithPosition);
+ }
+ }
+
+ /**
+ * A Marker and its position. {@link 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();
+
+ /**
+ * Animates a markerWithPosition from one position to another. TODO: improve performance for
+ * slow devices (e.g. Nexus S).
+ */
+ 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 boolean mRemoveOnComplete;
+ private MarkerManager mMarkerManager;
+
+ private AnimationTask(MarkerWithPosition markerWithPosition, LatLng from, LatLng to) {
+ this.markerWithPosition = markerWithPosition;
+ this.marker = markerWithPosition.marker;
+ this.from = from;
+ this.to = to;
+ }
+
+ public void perform() {
+ ValueAnimator valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
+ valueAnimator.setInterpolator(ANIMATION_INTERP);
+ valueAnimator.setDuration(mAnimationDurationMs);
+ valueAnimator.addUpdateListener(this);
+ valueAnimator.addListener(this);
+ valueAnimator.start();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mRemoveOnComplete) {
+ mMarkerCache.remove(marker);
+ mClusterMarkerCache.remove(marker);
+ mMarkerManager.remove(marker);
+ }
+ markerWithPosition.position = to;
+ }
+
+ public void removeOnAnimationComplete(MarkerManager markerManager) {
+ mMarkerManager = markerManager;
+ mRemoveOnComplete = true;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ float fraction = valueAnimator.getAnimatedFraction();
+ 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/main/java/com/google/maps/android/collections/MarkerManager.java b/library/src/main/java/com/google/maps/android/collections/MarkerManager.java
index 48d785648..cd8ca5215 100644
--- a/library/src/main/java/com/google/maps/android/collections/MarkerManager.java
+++ b/library/src/main/java/com/google/maps/android/collections/MarkerManager.java
@@ -19,6 +19,8 @@
import android.view.View;
import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.model.AdvancedMarker;
+import com.google.android.gms.maps.model.AdvancedMarkerOptions;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
@@ -145,6 +147,11 @@ public Marker addMarker(MarkerOptions opts) {
return marker;
}
+ public Marker addMarker(AdvancedMarkerOptions opts) {
+ Marker marker = mMap.addMarker(opts);
+ super.add(marker);
+ return marker;
+ }
public void addAll(java.util.Collection opts) {
for (MarkerOptions opt : opts) {
addMarker(opt);