Android Google Maps Utils ClusterManager Fit All Clusters

August 14, 2017
Use LatLngBounds.Builder and hack DefaultClusterRenderer.

Assuming you are using Google Maps Utils Cluster Manager to display markers as clusters and you would like the map to display all the clusters within the map view.

Google Maps fit all markers

You could utilize the Google Maps way of fitting all markers within the map view (adjustng zoom and position).

// if builder doesn't include any item, an exception shall be thrown
if (clusterItems.size() > 0) { 
  LatLngBounds.Builder builder = new LatLngBounds.Builder();
  for (ClusterItem item : clusterItems) {
    builder.include(item.getPosition());
  }
  LatLngBounds bounds = builder.build();

  // padding in pixels
  int padding = 20;
  // use animateCamera if animation is required
  map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, padding));

  if (clusterItems.size() == 1) {
    // you might want to a custom zoom level if there is only 1 item
    map.moveCamera(CameraUpdateFactory.zoomTo(15));
  }
}

If your markers or clusters are spread across the world (e.g. Japan, Europe & US), this method might not work very well as Google Maps can only show about half the world in its view.

Hack DefaultClusterRenderer

DefaultClusterRenderer does not expose most of its variables and functions (private only), so the only way to expose them is to copy the code and modify them with your own implementation.

Copy the code of DefaultClusterRenderer and rename the class as MyClusterRenderer. Add the following method to explose all the clusters.

public class MyClusterRenderer<T extends ClusterItem> implements ClusterRenderer<T> {
    ...
    public Collection<Cluster<T>> getClusters() {
        return  mClusterToMarker.keySet();
    }
    ...
}

Rather than looping all the markers to fit all clusters to be displayed in the map view, we shall loop through the clusters instead to get the LatLngBounds.

mapFragment.getMapAsync(new OnMapReadyCallback() {
    @Override
    public void onMapReady(final GoogleMap map) {
        ClusterManager clusterManager = new ClusterManager(activity, map);
        final MyClusterRenderer renderer = new NumberClusterRenderer(activity, map, clusterManager);
        // use this to show cluster only without markers
        renderer.setMinClusterSize(0);
        clusterManager.setRenderer(renderer);

        clusterManager.addItems(items);

        // clusters are not loaded until MapLoadedCallback
        map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
            @Override
            public void onMapLoaded() {
                Collection<Cluster> clusters = renderer.getClusters();
                if (clusters.size() > 0) {
                    LatLngBounds.Builder builder = new LatLngBounds.Builder();
                    for (Cluster cluster : clusters) {
                        builder.include(cluster.getPosition());
                    }
                    LatLngBounds bounds = builder.build();

                    // use 50dip to cater for the cluster icon size
                    float density = context.getResources().getDisplayMetrics().density;
                    int padding = (int) (50 * density);

                    CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, Helper.convertDpToPx(context, padding));
                    map.moveCamera(cu);
                }
            }
        });
    }
});    

Fit clusters and markers around the world

If the clusters are spread across the world, this method might not work very well as Google Maps can only show about half the world in its view.

First, we need to check if the map view can fit all the clusters. If it can’t fit, we shall focus the map view on the largest cluster while maintaining zoom (most likely the widest zoom).

mapFragment.getMapAsync(new OnMapReadyCallback() {
    @Override
    public void onMapReady(final GoogleMap map) {
        ClusterManager clusterManager = new ClusterManager(activity, map);
        final MyClusterRenderer renderer = new NumberClusterRenderer(activity, map, clusterManager);
        // use this to show cluster only without markers
        renderer.setMinClusterSize(0);
        clusterManager.setRenderer(renderer);

        clusterManager.addItems(items);

        // clusters are not loaded until MapLoadedCallback
        map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
            @Override
            public void onMapLoaded() {
                Collection<Cluster> clusters = renderer.getClusters();
                if (clusters.size() > 0) {
                    LatLngBounds.Builder builder = new LatLngBounds.Builder();
                    for (Cluster cluster : clusters) {
                        builder.include(cluster.getPosition());
                    }
                    LatLngBounds bounds = builder.build();

                    // use 50dip to cater for the cluster icon size
                    float density = context.getResources().getDisplayMetrics().density;
                    int padding = (int) (50 * density);

                    CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, Helper.convertDpToPx(context, padding));
                    map.moveCamera(cu);

                    // check can fit all the clusters
                    LatLngBounds mapBounds = map.getProjection().getVisibleRegion().latLngBounds;
                    if (mapBounds.contains(bounds.northeast) && mapBounds.contains(bounds.southwest)) {
                        Log.d(TAG, "Fit OK");
                    }
                    else {
                        Log.d(TAG, "Fit FAIL");
                        // loop all to find the biggest cluster
                        Cluster maxCluster = null;
                        for (Cluster cluster : clusters) {
                            if (maxCluster == null || cluster.getSize() > maxCluster.getSize()) {
                                maxCluster = cluster;
                            }
                        }
                        // focus on biggest cluster
                        if (maxCluster != null) {
                            cu = CameraUpdateFactory.newLatLng(maxCluster.getPosition());
                            map.moveCamera(cu);
                        }
                    }
                }
            }
        });
    }
});    
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.