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, MarkerOptions)} 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, MarkerOptions)} will not be called.
+ *
+ * @param item item to be rendered
+ * @param markerOptions the markerOptions representing the provided item
+ */
+ protected void onBeforeClusterItemRendered(@NonNull T item, @NonNull MarkerOptions markerOptions) {
+ if (item.getTitle() != null && item.getSnippet() != null) {
+ markerOptions.title(item.getTitle());
+ markerOptions.snippet(item.getSnippet());
+ } else if (item.getTitle() != null) {
+ markerOptions.title(item.getTitle());
+ } else if (item.getSnippet() != null) {
+ markerOptions.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, MarkerOptions)} 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, MarkerOptions)} 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, MarkerOptions)} will be called and
+ * {@link #onClusterUpdated(Cluster, Marker)} 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, Marker)} will be called and
+ * {@link #onBeforeClusterRendered(Cluster, MarkerOptions)} will not be called.
+ *
+ * @param cluster cluster to be rendered
+ * @param markerOptions markerOptions representing the provided cluster
+ */
+ protected void onBeforeClusterRendered(@NonNull Cluster cluster, @NonNull MarkerOptions markerOptions) {
+ // TODO: consider adding anchor(.5, .5) (Individual markers will overlap more often)
+ markerOptions.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, MarkerOptions)} and
+ * {@link #onClusterUpdated(Cluster, Marker)}.
+ *
+ * @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, MarkerOptions)} will be called and
+ * {@link #onClusterUpdated(Cluster, Marker)} 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, Marker)} will be called and
+ * {@link #onBeforeClusterRendered(Cluster, MarkerOptions)} 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 Marker 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()) {
+ Marker marker = mMarkerCache.get(item);
+ MarkerWithPosition markerWithPosition;
+ LatLng currentLocation = item.getPosition();
+ if (marker == null) {
+ MarkerOptions markerOptions = new MarkerOptions();
+ if (animateFrom != null) {
+ markerOptions.position(animateFrom);
+ } else if (mClusterMarkerCache.mCache.keySet().iterator().hasNext() && mClusterMarkerCache.mCache.keySet().iterator().next().getItems().contains(item)) {
+ T foundItem = null;
+ for (Cluster cluster : mClusterMarkerCache.mCache.keySet()) {
+ for (T clusterItem : cluster.getItems()) {
+ if (clusterItem.equals(item)) {
+ foundItem = clusterItem;
+ break;
+ }
+ }
+ }
+ currentLocation = foundItem.getPosition();
+ markerOptions.position(currentLocation);
+ } else {
+ markerOptions.position(item.getPosition());
+ if (item.getZIndex() != null) {
+ markerOptions.zIndex(item.getZIndex());
+ }
+ }
+ onBeforeClusterItemRendered(item, markerOptions);
+ marker = mClusterManager.getMarkerCollection().addMarker(markerOptions);
+ markerWithPosition = new MarkerWithPosition<>(marker, item);
+ mMarkerCache.put(item, marker);
+ if (animateFrom != null) {
+ markerModifier.animate(markerWithPosition, animateFrom, item.getPosition());
+ } else if (currentLocation != null) {
+ markerModifier.animate(markerWithPosition, currentLocation, item.getPosition());
+ }
+ } else {
+ markerWithPosition = new MarkerWithPosition<>(marker, item);
+ markerModifier.animate(markerWithPosition, marker.getPosition(), item.getPosition());
+ }
+ onClusterItemRendered(item, marker);
+ newMarkers.add(markerWithPosition);
+ }
+ return;
+ }
+
+ // Handle cluster markers
+ Marker marker = mClusterMarkerCache.get(cluster);
+ MarkerWithPosition markerWithPosition;
+ if (marker == null) {
+ MarkerOptions markerOptions = new MarkerOptions().position(animateFrom == null ? cluster.getPosition() : animateFrom);
+ onBeforeClusterRendered(cluster, markerOptions);
+ marker = mClusterManager.getClusterMarkerCollection().addMarker(markerOptions);
+ mClusterMarkerCache.put(cluster, marker);
+ markerWithPosition = new MarkerWithPosition(marker, null);
+ if (animateFrom != null) {
+ markerModifier.animate(markerWithPosition, animateFrom, cluster.getPosition());
+ }
+ } else {
+ markerWithPosition = new MarkerWithPosition(marker, null);
+ 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 final T clusterItem;
+ private LatLng position;
+
+ private MarkerWithPosition(Marker marker, T clusterItem) {
+ this.marker = marker;
+ this.clusterItem = clusterItem;
+ 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 ValueAnimator valueAnimator;
+
+ 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.ofFloat(0.0f, 1.0f);
+ valueAnimator.setInterpolator(ANIMATION_INTERP);
+ valueAnimator.setDuration(mAnimationDurationMs);
+ valueAnimator.addUpdateListener(this);
+ valueAnimator.addListener(this);
+ valueAnimator.start();
+ }
+
+ public void cancel() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ new Handler(Looper.getMainLooper()).post(this::cancel);
+ return;
+ }
+ markerWithPosition.position = to;
+ mRemoveOnComplete = false;
+ valueAnimator.cancel();
+ ongoingAnimations.remove(this);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mRemoveOnComplete) {
+ mMarkerCache.remove(marker);
+ mClusterMarkerCache.remove(marker);
+ mMarkerManager.remove(marker);
+ }
+ markerWithPosition.position = to;
+
+ // Remove the task from the queue
+ ongoingAnimations.remove(this);
+ }
+
+ public void removeOnAnimationComplete(MarkerManager markerManager) {
+ mMarkerManager = markerManager;
+ mRemoveOnComplete = true;
+ }
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator valueAnimator) {
+ if (to == null || from == null || marker == null) {
+ return;
+ }
+ 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);
+ markerWithPosition.position = position;
+ }
+ }
+}
diff --git a/library/src/main/java/com/google/maps/android/clustering/view/DefaultClusterRenderer.java b/library/src/main/java/com/google/maps/android/clustering/view/DefaultClusterRenderer.java
index 4d09c720d..f289c2796 100644
--- a/library/src/main/java/com/google/maps/android/clustering/view/DefaultClusterRenderer.java
+++ b/library/src/main/java/com/google/maps/android/clustering/view/DefaultClusterRenderer.java
@@ -145,19 +145,11 @@ public DefaultClusterRenderer(Context context, GoogleMap map, ClusterManager
@Override
public void onAdd() {
- mClusterManager.getMarkerCollection().setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
- @Override
- public boolean onMarkerClick(@NonNull Marker marker) {
- return mItemClickListener != null && mItemClickListener.onClusterItemClick(mMarkerCache.get(marker));
- }
- });
+ mClusterManager.getMarkerCollection().setOnMarkerClickListener(marker -> mItemClickListener != null && mItemClickListener.onClusterItemClick(mMarkerCache.get(marker)));
- mClusterManager.getMarkerCollection().setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() {
- @Override
- public void onInfoWindowClick(@NonNull Marker marker) {
- if (mItemInfoWindowClickListener != null) {
- mItemInfoWindowClickListener.onClusterItemInfoWindowClick(mMarkerCache.get(marker));
- }
+ mClusterManager.getMarkerCollection().setOnInfoWindowClickListener(marker -> {
+ if (mItemInfoWindowClickListener != null) {
+ mItemInfoWindowClickListener.onClusterItemInfoWindowClick(mMarkerCache.get(marker));
}
});
@@ -1168,7 +1160,7 @@ public void removeOnAnimationComplete(MarkerManager markerManager) {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
if (to == null || from == null || marker == null) {
- return;
+ return;
}
float fraction = valueAnimator.getAnimatedFraction();