android – ClusterManager repaint markers of Google maps v2 utils-ThrowExceptions

Exception or error:

Hi Im making a server request and when I received the request from server, I’m executing on Ui Thread a ClusterManager.addItem() but this items are not painting in the map, only when I make a zoom update (+,-) are showing. Also, I tried to debug the renderer, but onBeforeClusterRendered / onBeforeClusterItemRendered are not called until I update the zoom in map.
Any ideas how to refresh map/clusterManager/markers?

        MarkerManager markerManager = new MarkerManager(map);
        clusterManager = new ClusterManager<TweetClusterItem>(getActivity(), map, markerManager);
        clusterManager.setRenderer(new TweetClusterRenderer(getActivity(), map, clusterManager, defaultMarker));
        clusterManager.setOnClusterClickListener(this);
        clusterManager.setOnClusterInfoWindowClickListener(this);
        clusterManager.setOnClusterItemClickListener(this);
        clusterManager.setOnClusterItemInfoWindowClickListener(this);

        UiSettings uiSettings = map.getUiSettings();
        uiSettings.setZoomControlsEnabled(true);
        uiSettings.setMyLocationButtonEnabled(false);

        map.setOnCameraChangeListener(clusterManager);
        map.setOnMarkerClickListener(clusterManager);
        map.setOnInfoWindowClickListener(clusterManager);
        map.setOnMapClickListener(this);
How to solve:

mClusterManager.cluster();

force re-clustering items when you after added new item.

###

Seems that I found a workaround.

ClusterManager uses a renderer, in this case it inherits from DefaultClusterRenderer which uses a internal cache, a cache of markers that are added to map. You can access directly to the added markers on the map, I don’t use the info window, so i add marker options.title() an ID for later find this marker, so:

@Override
protected void onBeforeClusterItemRendered(TweetClusterItem item, MarkerOptions markerOptions) {

     .... Blabla code....          
            markerOptions.title(Long.toString(tweet.getId()));
     .... Blabla code....


}

and when I want to reload the clusterItem I call this method:

/**
  * Workarround to repaint markers
  * @param item item to repaint
 */
  public void reloadMarker(TweetClusterItem item) {

        MarkerManager.Collection markerCollection = clusterManager.getMarkerCollection();
        Collection<Marker> markers = markerCollection.getMarkers();
        String strId = Long.toString(item.getTweet().getId());
        for (Marker m : markers) {
            if (strId.equals(m.getTitle())) {
                m.setIcon( ICON TO SET);
                break;
            }
        }

    }

Maybe is a little hacky but it works and I din’t found any other way to do this. If you found another better way, please share 🙂

###

You can get specific markers that correspond to their cluster or cluster items and vice versa in O(1) using DefaultClusterRenderer’s getMarker(), getCluster() and getClusterItem() (set your own renderer to access the renderer object).

Use these methods to change the markers of your items whenever you need.

   ...
   DefaultClusterRenderer mRenderer = ...
   mClusterManager.setRenderer(mRenderer);
   ...

public void reloadMarker(ClusterItem item) {
    mRenderer.getMarker(item).setIcon(YOUR_ICON);
}

I wouldn’t recommend saving them anywhere else though, since those methods return the renderer’s cache objects.

###

I was having the same exact problem. None of the suggested solutions were working for me. I made a class which extends the DefaultClusterRenderer and adds the public method updateClusterItem(ClusterItem clusterItem) which will force the Marker associated with that ClusterItem to be re-rendered (works with both clusters and cluster items).

import android.content.Context;
import android.support.annotation.CallSuper;
import android.support.annotation.NonNull;

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
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.view.DefaultClusterRenderer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;


public abstract class CustomClusterRenderer<T extends ClusterItem>
        extends DefaultClusterRenderer<T> {

    private ClusterManager<T> mClusterManager;
    private Map<T, Marker> mClusterMap = new HashMap<>();

    public CustomClusterRenderer(Context context, GoogleMap map,
                                 ClusterManager<T> clusterManager) {
        super(context, map, clusterManager);
        mClusterManager = clusterManager;
    }


    @Override
    @CallSuper
    protected void onClusterItemRendered(T clusterItem, Marker marker) {
        super.onClusterItemRendered(clusterItem, marker);
        mClusterMap.remove(clusterItem);
        cleanCache();
    }

    @Override
    @CallSuper
    protected void onClusterRendered(Cluster<T> cluster, Marker marker) {
        super.onClusterRendered(cluster, marker);
        for (T clusterItem : cluster.getItems()) {
            mClusterMap.put(clusterItem, marker);
        }
        cleanCache();
    }

    public void updateClusterItem(T clusterItem) {
        Marker marker = getMarker(clusterItem);
        boolean isCluster = false;
        if (marker == null) {
            marker = mClusterMap.get(clusterItem);
            isCluster = marker != null;
        }
        if (marker != null) {
            MarkerOptions options = getMarkerOptionsFromMarker(marker);
            if (isCluster) {
                Cluster cluster = getCluster(marker);
                onBeforeClusterRendered(cluster, options);
            } else {
                onBeforeClusterItemRendered(clusterItem, options);
            }
            loadMarkerWithMarkerOptions(marker, options);
        }
    }

    private void cleanCache() {
        ArrayList<T> deleteQueue = new ArrayList<>();
        Collection<Marker> clusterMarkers = mClusterManager
                .getClusterMarkerCollection().getMarkers();

        for (T clusterItem : mClusterMap.keySet()) {
            if (!clusterMarkers.contains(mClusterMap.get(clusterItem))) {
                deleteQueue.add(clusterItem);
            }
        }

        for (T clusterItem : deleteQueue) {
            mClusterMap.remove(clusterItem);
        }
        deleteQueue.clear();
    }

    private MarkerOptions getMarkerOptionsFromMarker(@NonNull Marker marker) {
        MarkerOptions options = new MarkerOptions();

        options.alpha(marker.getAlpha());
        options.draggable(marker.isDraggable());
        options.flat(marker.isFlat());
        options.position(marker.getPosition());
        options.rotation(marker.getRotation());
        options.title(marker.getTitle());
        options.snippet(marker.getSnippet());
        options.visible(marker.isVisible());
        options.zIndex(marker.getZIndex());

        return options;
    }

    private void loadMarkerWithMarkerOptions(@NonNull Marker marker,
                                             @NonNull MarkerOptions options) {
        marker.setAlpha(options.getAlpha());
        marker.setDraggable(options.isDraggable());
        marker.setFlat(options.isFlat());
        marker.setPosition(options.getPosition());
        marker.setRotation(options.getRotation());
        marker.setTitle(options.getTitle());
        marker.setSnippet(options.getSnippet());
        marker.setVisible(options.isVisible());
        marker.setZIndex(options.getZIndex());
        marker.setIcon(options.getIcon());
        marker.setAnchor(options.getAnchorU(), options.getAnchorV());
        marker.setInfoWindowAnchor(options.getInfoWindowAnchorU(), options.getInfoWindowAnchorV());
    }

}

###

I had the same problem. It was also compounded by the fact I’m doing custom rendering in onBeforeClusterItemRendered on my DefaultClusterRenderer subclass.

My solution was to create a new instance of my DefaultClusterRenderer subclass and call setRenderer on the ClusterManager again. This dumps all the cached icons & recreates everything.

It’s hacky, brute force and annoyingly inefficient, but it does work. It was the only approach I found that worked since the library seems to have no explicit support for this.

###

mClusterManager.cluster(); didn’t work for me

this did though:

if (mMap != null) {
    CameraPosition currentCameraPosition = mMap.getCameraPosition();
    mMap.moveCamera(CameraUpdateFactory.newCameraPosition(currentCameraPosition));
}

This triggered an onCameraChange call, where I was already doing mClusterManager.clearItems()… mClusterManager.addItem(..) – for objects inside the visible region… mClusterManager.cluster()

The context for me was that the pins were disappearing when returning back to the fragment displaying the map (- only on certain devices e.g. Nexus 7, where there was no automatic call to OnCameraChange)

###

I noticed that the marker only showed up when zooming in or out, so I set a new camera position with all the old values except for a slight zoom change.

    CameraPosition currentCameraPosition = googleMap.getCameraPosition();
    CameraPosition cameraPosition = new CameraPosition(currentCameraPosition.target, currentCameraPosition.zoom - .1f, currentCameraPosition.tilt, currentCameraPosition.bearing);
    googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));

###

My solution with a CustomRenderer that extends DefaultClusterRenderer

 protected void onClusterItemRendered(T clusterItem, Marker marker) {
    marker.setIcon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_GREEN));
}

Leave a Reply

Your email address will not be published. Required fields are marked *