Tag

Custom Google Maps Marker

Browsing

Google Maps are a great way to show data. It might seem natural that the more geographical point Markers we add the better story it tells. But that’s not always the ideal case because once we’ve hundreds, even more, points the performance of Android application quickly begins to decrease. Let’s see an overloaded marker picture of Google Maps.

Google Maps With So Many MarkersWe all have probably seen a map picture like this. There are so many markers on Google Maps, you can’t even see the map. The colors of markers tell some different kind of story, but there’s so much to overlap with other icons that you might well just be missing other data.

One way of getting rid of this problem to grouped (cluster) the marker’s on a Google Map and show only a specific amount of them. To see how marker clustering looks alike see the below picture.

Google Map Marker Clustering

The number on a cluster indicates how many markers it contains. Once we zoom onto cluster location we’ll able see the markers.

So, this tutorial shows you how to use marker clustering to display a large number of markers on Google Map in an Android application. Enough of this intro on marker clustering let’s dive into Android Studio and start building our application.

Note: I assume that you have successfully set-up Google Maps API in your Android app. If not, then visit Google Maps API get started section.

Getting Started

Before start coding our application we need to add the marker clustering util library (provided by Google) in the app-level build.gradle file.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
 
    ........
    ........
   
    // MapV2 clustring marker dependency
    implementation 'com.google.maps.android:android-maps-utils:0.5'

    // Google play services location dependency
    implementation 'com.google.android.gms:play-services-location:16.0.0'
}

Sync the project and everything should be fine without any kind of gradle error.

Experiment with marker Clustering

Now that everything is done let’s see how the marker clustering done on Google Maps. The simplest way to show the cluster, first, we need to implement ClusterItem on our model class. Let’s create a simple User class that implements ClusterItem.

public class User implements ClusterItem {

    private final String username;
    private final LatLng latLng;

    public User(String username, LatLng latLng) {
        this.username = username;
        this.latLng = latLng;
    }

    @Override
    public LatLng getPosition() {  // 1
        return latLng;
    }

    @Override
    public String getTitle() {  // 2
        return username;
    }

    @Override
    public String getSnippet() {
        return "";
    }
}

Here’s what going on inside the above code.

  1. The ClusterItem returns the position of the marker, which later Google Maps use’s and show the marker. Must always return same LatLng position.
  2. Title of the marker which will be visible when you click on a single marker.

Next, we need to add the ClusterManager inside our MapActivity.

public class MapActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SupportMapFragment supportMapFragment = (SupportMapFragment)      getSupportFragmentManager().findFragmentById(R.id.map_fragment);  // 1
        supportMapFragment.getMapAsync(new OnMapReadyCallback() {  // 2
            @Override
            public void onMapReady(GoogleMap googleMap) {
                setUpClusterManager(googleMap);
            }
        });
    }

    private void setUpClusterManager(GoogleMap googleMap){
        ClusterManager<User> clusterManager = new ClusterManager(this, googleMap);  // 3
        googleMap.setOnCameraIdleListener(clusterManager);
        List<User> items = getItems();
        clusterManager.addItems(items);  // 4
        clusterManager.cluster();  // 5
    } 

    private List<User> getItems() {
       ......  // returns the collection of users.
    }
}

Taking each commented section in turn.

  1. Obtain the SupportMapFragment from xml.
  2. Registering a callback to get notified when we’re ready to use Google Maps.
  3. Creates a new ClusterManager which later handles the clustering for our markers. Note the type of argument<User> which declares the ClusterManager to be of User type.
  4. Feed the items to our newly created ClusterManager.
  5. Call the cluster method, you may want to call this method after feeding items to ClusterManager.

Here’s the demo of our application with marker clustering and you can get the finished code for Default Marker Clustering from here.

Custom marker inside the Marker Clustering

Since weren’t satisfied with the default solution, we need to make some customization to our default setup. Now let’s say I need to change the red default marker icon which shows inside the Cluster Marker with my own custom marker. For that, we need to create a new ClusterRenderer with to extend the DefaultClusterRenderer class.

Here, let’s create a new ClusterManager class to show our custom marker instead of default ones.

public class MarkerClusterRenderer extends DefaultClusterRenderer<User> {   // 1

    private static final int MARKER_DIMENSION = 48;  // 2

    private final IconGenerator iconGenerator;
    private final ImageView markerImageView;

    public MarkerClusterRenderer(Context context, GoogleMap map, ClusterManager<User> clusterManager) {
        super(context, map, clusterManager);
        iconGenerator = new IconGenerator(context);  // 3
        markerImageView = new ImageView(context);
        markerImageView.setLayoutParams(new ViewGroup.LayoutParams(MARKER_DIMENSION, MARKER_DIMENSION));
        iconGenerator.setContentView(markerImageView);  // 4
    }

    @Override
    protected void onBeforeClusterItemRendered(User item, MarkerOptions markerOptions) { // 5
        markerImageView.setImageResource(R.drawable.location_vector_icon);  // 6
        Bitmap icon = iconGenerator.makeIcon();  // 7
        markerOptions.icon(BitmapDescriptorFactory.fromBitmap(icon));  // 8
        markerOptions.title(item.getTitle());
    }
} 

Here’s what’s going on inside the MarkerClusterRenderer class.

  1. Extending the default renderer in order to customize the marker’s inside the cluster.
  2. Setting the constant value for the single marker size.
  3. IconGenerator a utility class to generate the Bitmap icon marker.
  4. Adding the child view for IconGenerator class and in our case the ImageView which simply shows the marker.
  5. Called before every ClusterItem added to the Google Maps.
  6. Sets the drawable resource as the content of this marker ImageView.
  7. Generates the Bitmap of the previously custom view setting.
  8. Sets the icon for the marker.

Now we just need to set the instance of MarkerClusterRenderer class to our ClusterManager inside the MapActivity.

private void setUpClusterManager(GoogleMap googleMap) {
        ClusterManager<User> clusterManager = new ClusterManager<>(this, googleMap);
        clusterManager.setRenderer(new MarkerClusterRenderer(this, googleMap, clusterManager));
        ......
        ......
}

After adding the MarkerClusterRenderer class instance here’s the demo of our application with custom markers inside the Marker Clustering. You can get the complete code of Custom Marker Clustering from this link.

I hope this article will help you do custom Marker Clustering on Google Maps with Android application. Anything, I miss in this article please let me know via the comments section.

Thank you for being here and keep reading…

This story is the third part of our Android Cab Booking App tutorial If you didn’t read the previous ones you can start here.

Previously In Android Cab Booking App

In the previous article, we discussed how to read the online-drivers from Firebase Realtime Database, create the data structure for Drivers and animate multiples markers on Google Maps.

Also, I update the apps on GitHub. You can follow me by looking at the code. Java application link and Kotlin application link.

9. Show progress animation inside PinView

If you guys see the demo in the first article, you’ll understand that as soon as the Google Maps camera movement start. The ProgressBar inside the PinView start animating until the nearest Driver found and after that, we’ll calculate the distance with DistanceMatrixAPI between the nearest Driver location and the PinView location. Later the distance calculation we hide the ProgressBar and show the distance duration inside the PinView.

So, in order to animate the ProgressBar, we need to implement OnCameraMoveStartedListener on MainActivity. The OnCameraMoveStartedListener has its callback function which defines what action needs to be done when the camera starts its movement. In this callback, we need to start the ProgressBar animation.

Let’s start the ninth part of Cab Booking Application by implementing the OnCameraMoveStartedListener on MainActivity.

class MainActivity : AppCompatActivity(), GoogleMap.OnCameraIdleListener, GoogleMap.OnCameraMoveStartedListener {
      private var googleMap : GoogleMap? = null
      override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            .......
            .......
            val supportMapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment supportMapFragment.getMapAsync { googleMap ->
                    googleMap.setOnCameraIdleListener(this)
                    googleMap.setOnCameraMoveStartedListener(this)  // implements the OnCameraStartedListener                        
                    this.googleMap = googleMap 
            }
     }

The OnCameraMoveStartedListener has one abstract method and we need to implement it as well. Below is the onCameraMoveStarted overridden method.

override fun onCameraMoveStarted(p: Int) {
     pinTimeTextView.visibility = GONE
     pinProgressLoader.visibility = VISIBLE
}

In the onCameraMoveStarted method, we show the ProgressBar and hide the previously shown TextView. Now if you run the application, you’ll see that when the app opens the ProgressBar start animating inside the PinView.Google Maps PinView Animation

10. Get the nearest driver and calculate the Distance

In order to get the nearest Driver, we need to know the PinView location exactly where the user stops while dragging it. If you guy’s remembered from the previous article, we exactly face the same problem when we need to Reverse Geocode a location where user drags the Pin. So, for this, we need to update our onCameraIdle method inside the MainActivity.

Update the onCameraIdle method inside the MainActivity class.

override fun onCameraIdle() {
    val position = googleMap.cameraPosition.target
    viewModel.makeReverseGeocodeRequest(position, geoCoderValue.value)
    viewModel.onCameraIdle(latLng);
}

Next, add the below method inside the MainActivityViewModel class.

fun onCameraIdle(latLng: LatLng) {
        launch(coroutineContext + Dispatchers.Default) {
            if (driverRepo.allItems().isNotEmpty()) {
                val driver = driverRepo.getNearestDriver(latLng.latitude, latLng.longitude)
                driver?.let { calculateDistance(latLng, it) }
            }
        }
    }

At first, we launch a coroutine because the getNearestDriver method is a suspended method. After that, we check if the List<Driver> that we’re keeping inside the DriverCollection is not empty. Later we call the getNearestDriver method inside DriverCollection and call the calculateDistance method if the nearest Driver is not null.

You can get the DriverCollection class from GitHub.

Add the below calculateDistance method inside the MainActivityViewModel class.

private fun calculateDistance(latLng: LatLng, driver: Driver) {
    launch(coroutineContext + Dispatchers.IO) {  // 1
        val destination = arrayOf(driver.lat.toString() + "," + driver.lng.toString())
        val origins = arrayOf(latLng.latitude.toString() + "," + latLng.longitude.toString())
        DistanceMatrixApi.getDistanceMatrix(googleMapHelper.geoContextDistanceApi(), origins, destination)  // 2
            .mode(TravelMode.DRIVING)
            .setCallback(object : PendingResult.Callback<DistanceMatrix> {

                override fun onFailure(e: Throwable?) {
                }

                override fun onResult(result: DistanceMatrix?) {
                    if (result != null)
                        _calculateDistance.postValue(result.rows[0].elements[0].duration.humanReadable)  // 3
                }
            })
    }
}

Let’s go through the logic behind the above code:

  1. launch a coroutine in IO dispatcher, because it is a good approach to execute all network requests inside the IO dispatcher.
  2. The DistanceMatrixApi class is from the dependency which we add in the first article. You can read more about how to use the library here on GitHub.
  3. If the DistanceMatrix result is not null then we pass the result to MainActivity via LiveData to show the distance duration inside the PinView.

Note: The distance we’re calculating is from Driver location to PinView location. That’s how the Careem application shows the calculated distance inside PinView.

Next, add the geoContextDistanceApi method inside the GoogleMapHelper class.

class GoogleMapHelper(private val resources : Resources) {

   companion object {
       private val geoApiContextBuilder = GeoApiContext.Builder()
   }

   .......
   .......

   private fun distanceApi(): String {
        return resources.getString(R.string.google_distance_matrix_api_key)  // replace with your own distance matrix api key.
   }

   fun geoContextDistanceApi(): GeoApiContext {
        return geoApiContextBuilder
            .apiKey(distanceApi())
            .build()
   }
}

Now that we’ve added the utility method inside the GoogleMapHelper class. We need to declare _calculateDistance inside MainActivityViewModel class in order to observe from MainActivity.

private val _calculateDistance = MediatorLiveData<String>()

val calculateDistance : LiveData<String> = _calculateDistance

Next, add the following code inside the MainActivity onCreate method to observe calculateDistance instance from MainActivityViewModel class.

// 1
viewModel.calculateDistance
            .observe(this, Observer { distance ->
                   pinTimeTextView.text = distance
                   pinTimeTextView.visibility = VISIBLE
                   pinProgressLoader.visibility = GONE
            })

When the Observer interface invoked we simply set the distance to pinTimeTextView and hide the currently showing pinProgressLoader.

So, here we’ve completed the tenth part of the Frisbee application. Now when you run the application you’ll see the nearest Driver duration inside the PinView and also the duration updates when the Google Maps camera movement stops. Build and run to view the application progress. Here is mine:

Google Map Custom PinView With Text

Bravo! this concludes that our Frisbee application is complete. Hopefully, the next article will be on Driver application and it’ll be the last article on Android cab booking app tutorial. Stay tuned!

Thank you for being here and keep reading…

Previous Part