Blog Infos
Author
Published
Topics
, , , ,
Published

This article explains how to add interactive elements to maps in Android applications using the Google Maps SDK. It covers how to create and customize map components such as markers, polylines, polygons, and circles. Additionally, it provides practical tips for using marker clustering to improve the visual organization of map elements.

Photo by Tamas Tuzes-Katai on Unsplash

Adding Required Dependencies and Generating an API Key for Map Usage

To use Google Maps in Android applications, you need to add specific dependencies to your project.

The maps-ktx library allows you to leverage Kotlin language features while working with the Maps SDK on Android.

Add the following dependencies to your build.gradle (Module: app) file:

implementation("com.google.android.gms:play-services-maps:19.0.0")
implementation("com.google.maps.android:maps-ktx:5.1.1")

After adding the necessary dependencies, you need to create a project in the Google Cloud Platform (GCP) and generate an API key to enable Google Maps services.

1 – Generating an API Key on Google Cloud

To use the Google Maps API, follow these steps:

  1. Go to the Google Cloud Platform.
  2. Create a new project.
  3. From the left-hand menu, navigate to APIs & Services.
  4. Click on the Credentials tab.
  5. Click Create API Key to generate a new API key.
Securing Your API Key

For security purposes, it is recommended to restrict your API key by specifying:

  • Your application’s package name
  • The SHA-1 certificate fingerprint
Getting the SHA-1 Certificate Fingerprint

To obtain the SHA-1 fingerprint, open your terminal in the root directory of your Android project and run the following command:

./gradlew signingReport

After the command executes, the terminal will display the SHA-1 value. You can use this value in the API key restriction settings on Google Cloud.

2 – Adding the API Key to Your Application

To securely store your API key in your Android project, follow these steps:

  1. Use the Secrets Gradle Plugin

You can use the Secrets Gradle Plugin to manage your API key securely.

Add the following dependency to your project-level build.gradle or build.gradle.kts file:

buildscript {
    dependencies {
        classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1")
    }
}

2. Apply the Plugin

In your module-level build.gradle or build.gradle.kts file, apply the plugin:

plugins {
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}

3. Create a secrets.properties File

In the root directory of your project, create a file named secrets.properties and add the following line:

MAPS_API_KEY=YOUR_API_KEY

4. Add a Default Value

To avoid build errors when the API key is missing, create a local.defaults.properties file and add:

MAPS_API_KEY=DEFAULT_API_KEY

5. Add API Key to AndroidManifest.xml

Insert the following <meta-data> tag inside your <application> tag:

<application>
    ...
    <meta-data
        android:name="com.google.android.geo.API_KEY"
        android:value="${MAPS_API_KEY}" />
</application>

6. Configure the Plugin

In your module-level build.gradle file, configure the plugin:

android {
    secrets {
        propertiesFileName = "secrets.properties"
        defaultPropertiesFileName = "local.defaults.properties"
        ignoreList.add("keyToIgnore")
        ignoreList.add("sdk.*")
    }
}

After completing these steps, your API key will be securely integrated into your application.

3 – The New Map Renderer

With version 18.2.0 of the Google Maps SDK, a new map renderer was introduced. This renderer brings several improvements and benefits for developers:

  • Cloud-based Maps Styling: Allows dynamic updates to the visual styling of maps through the cloud.
  • Advanced Polyline Customization: Enables more refined and visually appealing line styles.
  • Reduced Network and Memory Usage: Enhances app efficiency by minimizing unnecessary data consumption.
  • Improved Motion Controls: Offers smoother animations, panning, and zooming experiences.
Backward Compatibility

Google ensures that devices not supporting the new renderer will continue to use the legacy renderer. Although you can work with version 18.2.0 or later, developers may opt to use the legacy renderer until March 2025. After this date, the choice will be removed, and Google will automatically apply the legacy renderer on unsupported devices.

Selecting a Renderer

You can specify whether to use the legacy or latest renderer with the following setup:

class MapRendererOptInApplication : Application(), OnMapsSdkInitializedCallback {
    override fun onCreate() {
        super.onCreate()
        // Select the renderer here (LEGACY or LATEST)
        MapsInitializer.initialize(applicationContext, Renderer.LEGACY, this)
    }

    override fun onMapsSdkInitialized(renderer: MapsInitializer.Renderer) {
        when (renderer) {
            Renderer.LATEST -> Log.d("MapsDemo", "The latest version of the renderer is used.")
            Renderer.LEGACY -> Log.d("MapsDemo", "The legacy version of the renderer is used.")
        }
    }
}

In this example, the MapsInitializer.initialize() method lets you define which renderer to use, while onMapsSdkInitialized() provides feedback on the active renderer at runtime.

Core Google Maps Classes

Here are some fundamental classes used when working with the Google Maps SDK in Android:

  • GoogleMap: The main entry point for interacting with the map’s data and features. It is only accessible via a SupportMapFragment or MapView.
  • SupportMapFragment: A fragment that manages the lifecycle of a GoogleMap instance.
  • MapView: A View-based alternative that also manages the GoogleMap lifecycle.
  • OnMapReadyCallback: An interface that is triggered once the map is ready to be used.

Among these, SupportMapFragment is more modern and widely adopted. We’ll continue using this approach in our implementation.

Step 1: Add the Fragment in XML

 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/mapView"
        android:name="com.google.android.gms.maps.SupportMapFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

 

Step 2: Implement OnMapReadyCallback in Your Activity

 

class MainActivity : AppCompatActivity(), OnMapReadyCallback {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val supportMapFragment = supportFragmentManager
            .findFragmentById(R.id.mapView) as SupportMapFragment

        supportMapFragment.getMapAsync(this)
    }

    override fun onMapReady(googleMap: GoogleMap) {
        googleMap.moveCamera(
            CameraUpdateFactory.newLatLngZoom(
                LatLng(41.0082, 28.9784), // Istanbul, Turkey
                15f
            )
        )
    }
}

 

In this setup:

  • We implement OnMapReadyCallback to detect when the map is fully loaded.
  • getMapAsync(this) initializes the map asynchronously.
  • Once ready, the map centers on a sample location (Istanbul, Turkey) using CameraUpdateFactory.newLatLngZoom() with a zoom level of 15x.

 

4 – Adding a Marker

To highlight a specific location on the map, you can use the Marker object. Markers are commonly used to provide users with a visual reference and additional contextual information.

Here’s how to add a basic marker:

googleMap.addMarker(
    MarkerOptions()
        .position(istanbul)
        .title("Istanbul")
        .snippet("Tarihi Yarımada")
        .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))
)

googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(istanbul, 12f))
Using a Custom Icon

The Google Maps SDK supports icons of type BitmapDescriptor. If you want to use your own image (like a vector drawable), you’ll first need to convert it to a Bitmap.

Convert Vector Drawable to Bitmap

 

fun vectorToBitmap(
    context: Context,
    @DrawableRes vectorResId: Int
): Bitmap {
    val drawable = ContextCompat.getDrawable(context, vectorResId)
    val bitmap = Bitmap.createBitmap(
        drawable?.intrinsicWidth ?: 0,
        drawable?.intrinsicHeight ?: 0,
        Bitmap.Config.ARGB_8888
    )
    val canvas = Canvas(bitmap)
    drawable?.setBounds(0, 0, canvas.width, canvas.height)
    drawable?.draw(canvas)
    return bitmap
}

 

Add a Marker with a Custom Bitmap Icon

 

googleMap.addMarker(
    MarkerOptions()
        .position(istanbul)
        .title("İstanbul")
        .snippet("Tarihi Yarımada")
        .icon(
            BitmapDescriptorFactory.fromBitmap(
                vectorToBitmap(this, R.drawable.ic_launcher_foreground)
            )
        )
)

 

This allows you to personalize marker icons with your own vector images.

Removing a Marker

In Google Maps SDK, to remove a marker, you need to keep a reference to the Marker object. The googleMap.addMarker() method returns this reference, which you can then use to remove the marker from the map:

val marker: Marker? = googleMap.addMarker(
    MarkerOptions()
        .position(LatLng(41.0082, 28.9784))
        .title("Istanbul")
        .snippet("Tarihi Yarımada")
)

// Remove the marker from the map
marker?.remove()
5 – Google Maps Camera Movements

The Google Maps SDK provides several ways to control the camera’s position and orientation on the map, enhancing user experience by focusing on specific areas.

Core Classes for Camera Control:
  • CameraUpdateFactory: Creates camera update objects.
  • GoogleMap.moveCamera(): Instantly moves the camera.
  • GoogleMap.animateCamera(): Moves the camera with animation.
moveCamera()

Quickly moves the camera to a specific location and zoom level:

val istanbul = LatLng(41.0082, 28.9784)
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(istanbul, 12f))

Zoom levels meaning:

  • 1–5: Global view
  • 10–15: City view
  • 16+: Street-level detail
animateCamera()

Smoothly animates the camera movement:

val ankara = LatLng(39.9334, 32.8597)
googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(ankara, 14f))
Rotate and Tilt the Camera

You can rotate (bearing) and tilt the camera for better perspectives:

val cameraPosition = CameraPosition.Builder()
    .target(LatLng(41.0082, 28.9784))
    .zoom(16f)
    .bearing(90f) // East (0 = North, 90 = East)
    .tilt(45f)    // Tilt angle (0-90 degrees)
    .build()

googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
Camera Bounds for Multiple Locations

To frame multiple locations within the viewport:

val bounds = LatLngBounds.builder()
    .include(LatLng(41.0082, 28.9784))  // Istanbul
    .include(LatLng(39.9334, 32.8597))  // Ankara
    .build()

googleMap.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 100))

This adjusts the camera to show both points with padding.

6 – Drawing a Polyline on Google Maps

Google Maps SDK allows you to create Polyline objects that connect two or more points on the map. Polylines are commonly used to visualize routes, paths, or boundaries.

Here is a simple example of how to draw a polyline:

val polylineOptions = PolylineOptions()
    .add(LatLng(40.989010, 29.022949))  // First point
    .add(LatLng(40.986484, 29.023937))  // Second point
    .color(ContextCompat.getColor(this, R.color.blue))  // Line color
    .width(10f)  // Line width in pixels

googleMap.addPolyline(polylineOptions)
  • Use .add() multiple times to include each point in the polyline.
  • Customize the polyline’s color and width for better visibility.
  • The polyline will be rendered as a connected line between the given points on the map.

You can make polylines more visually appealing by customizing their patterns using the pattern() method. Google Maps SDK supports these pattern elements:

  • Dot(): A dotted pattern.
  • Dash(length): A solid dash with the specified length in pixels.
  • Gap(length): A blank gap with the specified length in pixels.

Here’s an example of a polyline with a dashed pattern:

val polylineOptions = PolylineOptions()
    .add(LatLng(40.989010, 29.022949))
    .add(LatLng(40.986484, 29.023937))
    .color(ContextCompat.getColor(this, R.color.blue))
    .pattern(listOf(Dash(20f), Gap(20f), Dash(20f)))  // Dash-Gap-Dash pattern
    .width(10f)

googleMap.addPolyline(polylineOptions)

This creates a polyline with alternating dashes and gaps, enhancing the visual style of the line on the map.

Remove Polyline

To remove a polyline from the map, you need to keep a reference to the Polyline object returned by googleMap.addPolyline(). Then, you can call the remove() method on that reference:

val polyline: Polyline = googleMap.addPolyline(
    PolylineOptions()
        .add(LatLng(40.989010, 29.022949))
        .add(LatLng(40.986484, 29.023937))
        .color(ContextCompat.getColor(this, R.color.blue))
        .width(10f)
)

// Remove the polyline from the map
polyline.remove()

If you have multiple polylines or markers, managing them with unique IDs stored in a HashMap<String, Polyline> or HashMap<String, Marker> is useful. This way, you can find, update, or remove specific items by their ID easily.

7- Adding a Polygon

Polygons are similar to polylines but differ in that they form a closed shape and can have a filled interior. They are typically used to visually represent boundaries such as city limits, districts, or service areas on the map.

For example, if your app serves only specific neighborhoods or cities, you can draw these boundaries using polygons to clearly communicate the service area to users. Conversely, polygons can also highlight areas where service is not available. The fill color makes these regions visually distinct, improving user experience by preventing incorrect location selections.

Example of drawing a polygon with stroke and fill colors:

val polygonOptions = PolygonOptions()
    .add(LatLng(40.989010, 29.022949))
    .add(LatLng(40.986484, 29.023937))
    .add(LatLng(40.988638, 29.025182))
    .strokeColor(ContextCompat.getColor(this, R.color.red))  // Border color
    .strokeWidth(5f) 
    .fillColor(ContextCompat.getColor(this, R.color.light_red))  // Interior fill color

val polygon: Polygon = googleMap.addPolygon(polygonOptions)

This will draw a filled polygon on the map, clearly marking areas such as no-service zones, allowing users to distinguish between serviceable and non-serviceable regions easily.

Polygons can be customized with border patterns, similar to polylines. Border lines can be made aesthetic by using dotted, striped or spaced patterns. Deletion is also done by referencing the polygon object, as in polylines. In order to manage multiple polygons in the application, they can be stored in a collection by assigning an ID to each one and removed when necessary.

8 – Adding a Circle

 

val circleOptions = CircleOptions()
    .center(istanbul)
    .radius(200.0)
    .strokeColor(ContextCompat.getColor(this, R.color.blue))
    .strokeWidth(5f)
    .fillColor(ContextCompat.getColor(this, R.color.light_blue))
    .strokePattern(listOf(Dash(30f), Gap(20f)))


googleMap.addCircle(circleOptions)

 

Using Clustering in Google Maps SDK

When adding many markers to a Google Map, visual clutter and overlapping can reduce clarity. To solve this, the Cluster feature groups nearby markers into a single icon. As users zoom in, the cluster breaks apart and individual markers become visible.

Benefits of Clustering:

  1. Improved UX — Prevents overcrowding by grouping markers.
  2. Performance — Reduces rendering load when many markers are displayed.
  3. Scalability — Essential for apps showing dynamic or large datasets.

Clustering is especially useful in apps like delivery tracking, real estate, or social networks, where many pins can appear in close proximity.

Implementation typically uses the Android Maps Utils library, which provides a ClusterManager to manage and render marker clusters easily.

Sample Scenario: Vehicle Sharing App with Marker Selection and Clustering

In a vehicle-sharing service, we aim to display the locations of available vehicles on the map using individual markers, each with a unique vehicle icon. To manage a clean interface:

  1. Marker Display:
  • Each vehicle location is represented with a custom marker icon.
  • When a user taps on a vehicle, the app updates the selected marker with a distinct icon or color to indicate selection.
  1. Clustering Implementation:
  • When too many vehicles are shown in one area, clustering groups markers visually.
  • The cluster icon displays the number of grouped vehicles, e.g., +10+50.
  1. Technical Approach:
  • Use ClusterManager from the Google Maps Utils library.
  • Customize ClusterItem to represent each vehicle.
  • Listen for marker clicks to handle selection and update the marker icon accordingly.

This approach provides a user-friendly and performant map interface for real-time vehicle discovery.

Using Clustering

To enable clustering on Google Maps, you need to add the following dependency to your build.gradle (Module: app) file:

implementation("com.google.maps.android:android-maps-utils:3.9.0")

After adding the dependency, you need to create a data class that represents each individual item to be displayed on the map and included in the cluster. This class must implement the ClusterItem interface.

This approach allows the clustering utility to manage and group nearby items automatically, improving performance and map readability, especially when displaying a large number of markers.

data class CarClusterItem(
    val lat: Double,
    val lng: Double,
    val isSelected: Boolean,
    val clusterId: String = UUID.randomUUID().toString()
) : ClusterItem {

    override fun getPosition(): LatLng {
        return LatLng(lat, lng)
    }

    override fun getTitle(): String? {
        return null
    }

    override fun getSnippet(): String? {
        return null
    }

    override fun getZIndex(): Float? {
        return null
    }
}
Cluster Manager

 

class CarClusterManager<T : ClusterItem>(
    private val context: Context,
    private val map: GoogleMap
) : ClusterManager<T>(context, map) {

    companion object {
        private const val CLUSTER_MAX_ZOOM_LEVEL = 14
    }

    private var _shouldClusterZoom: Boolean = true
    val shouldClusterZoom get() = _shouldClusterZoom

    override fun onCameraIdle() {
        super.onCameraIdle()
        _shouldClusterZoom = map.cameraPosition.zoom < CLUSTER_MAX_ZOOM_LEVEL
    }
}

 

The CarClusterManager class extends the ClusterManager<T> class and is used to manage clustering behavior on the map. This custom class is responsible for configuring how and when markers on the map should be grouped into clusters.

A constant named CLUSTER_MAX_ZOOM_LEVEL defines the maximum zoom level at which clustering will be active. In this example, it is set to 14.

  • If the zoom level is less than 14, the _shouldClusterZoom variable is set to true, enabling clustering behavior.
  • If the zoom level is 14 or greater, the _shouldClusterZoom variable is set to false, disabling clustering and allowing markers to be displayed individually.

This logic helps improve map readability by dynamically switching between clustered and individual marker views based on the user’s zoom level.

Utility Functions for Cluster and Car Icons

 

object IconHelper {

    fun getClusterIcon(
        context: Context,
        cluster: Cluster<CarClusterItem>
    ): BitmapDescriptor {
        val bucketStr = getBucketString(cluster.size)
        val bmpClusterSize = vectorToBitmap(context, R.drawable.ic_cluster)
        val bitmap = bmpClusterSize.drawClusterCount(context, bucketStr, R.color.white)
        return BitmapDescriptorFactory.fromBitmap(bitmap)
    }

    fun getCarIcon(
        item: CarClusterItem,
        context: Context
    ): BitmapDescriptor? {
        val bitmap = if (item.isSelected) {
            R.drawable.ic_selected
        } else {
            R.drawable.ic_unselected
        }
        return vectorToBitmapDescriptor(context, bitmap)
    }

    private fun getBucketString(size: Int): String {
        return when {
            size > 999 -> "1000+"
            size > 499 -> "500+"
            size > 99 -> "100+"
            size > 49 -> "50+"
            size > 19 -> "20+"
            size > 9 -> "10+"
            else -> size.toString()
        }
    }

    private fun Bitmap.drawClusterCount(context: Context, text: String, color: Int): Bitmap {
        val bitmap = copy(config!!, true)
        val canvas = Canvas(bitmap)
        val paint = createPaint(context, color)
        val bounds = Rect()
        paint.getTextBounds(text, 0, text.length, bounds)
        val width = bitmap.width / 2
        val height = bitmap.height / 2.7
        canvas.drawText(text, width.toFloat(), height.toFloat(), paint)
        return bitmap
    }

    private fun createPaint(context: Context, color: Int): Paint {
        return Paint().apply {
            flags = Paint.ANTI_ALIAS_FLAG
            textAlign = Paint.Align.CENTER
            this.color = ContextCompat.getColor(context, color)
            this.textSize = context.resources.getDimensionPixelSize(R.dimen.text_size_9sp).toFloat()
            typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
        }
    }

    private fun vectorToBitmap(
        context: Context,
        @DrawableRes vectorResId: Int,
    ): Bitmap {
        val drawable = ContextCompat.getDrawable(context, vectorResId)
        val bitmap = Bitmap.createBitmap(
            drawable?.intrinsicWidth ?: 0,
            drawable?.intrinsicHeight ?: 0,
            Bitmap.Config.ARGB_8888
        )
        val canvas = Canvas(bitmap)
        drawable?.setBounds(0, 0, canvas.width, canvas.height)
        drawable?.draw(canvas)
        return bitmap
    }

    private fun vectorToBitmapDescriptor(
        context: Context,
        @DrawableRes vectorResId: Int
    ): BitmapDescriptor? {
        val bitmap = vectorToBitmap(context, vectorResId)
        return BitmapDescriptorFactory.fromBitmap(bitmap)
    }
}

 

Before moving on to implementing the ClusterRenderer, let’s look at a few utility functions that will help us define custom icons for cars and clusters.

  • getClusterIcon()
    This function generates an appropriate icon based on the size of the cluster. It visually represents the number of items contained within the cluster to the user.
  • getCarIcon()
    This function determines which car icon should be displayed on the map depending on whether the item is in a selected or unselected state.
  • vectorToBitmap()
    Converts a vector-based drawable resource (@DrawableRes) into a Bitmap object, which is required for custom marker icons on Google Maps.

These functions provide the visual foundation for customizing how items and clusters are displayed on the map.

Cluster Renderer

 

class CarClusterRenderer(
    private val context: Context,
    private val map: GoogleMap?,
    private val clusterManager: CarClusterManager<CarClusterItem>?,
) : DefaultClusterRenderer<CarClusterItem>(context, map, clusterManager) {

    override fun onBeforeClusterItemRendered(
        item: CarClusterItem,
        markerOptions: MarkerOptions,
    ) {
        super.onBeforeClusterItemRendered(item, markerOptions)
        markerOptions.icon(
            IconHelper.getCarIcon(
                item = item,
                context = context
            )
        )
    }

    override fun onClusterItemUpdated(item: CarClusterItem, marker: Marker) {
        super.onClusterItemUpdated(item, marker)
        marker.setIcon(
            IconHelper.getCarIcon(
                item = item,
                context = context
            )
        )
    }

    override fun onBeforeClusterRendered(
        cluster: Cluster<CarClusterItem>,
        markerOptions: MarkerOptions,
    ) {
        super.onBeforeClusterRendered(cluster, markerOptions)
        markerOptions.icon(IconHelper.getClusterIcon(context = context, cluster = cluster))
    }

    override fun onClusterUpdated(cluster: Cluster<CarClusterItem>, marker: Marker) {
        super.onClusterUpdated(cluster, marker)
        marker.setIcon(IconHelper.getClusterIcon(context = context, cluster = cluster))
    }

    override fun shouldRenderAsCluster(cluster: Cluster<CarClusterItem>): Boolean =
        cluster.size >= 4 && clusterManager?.shouldClusterZoom == true
}

 

The CarClusterRenderer class is a helper class used to customize how markers appear on the map. It extends the DefaultClusterRenderer class and is responsible for setting the icons of both individual car markers and clusters displayed on the map.

Let’s review the key functions:

  • onBeforeClusterItemRendered
    Called when a cluster item is added to the map for the first time. This is where you configure the marker’s appearance, such as its icon, title, or color.
  • onClusterItemUpdated
    Invoked when an existing marker on the map needs to be updated. For example, after clicking a marker, if you want to update its icon or reflect a change in its state (e.g., updating an isSelected property), this function handles the visual update dynamically.
  • onBeforeClusterRendered
    Called when a new cluster is created on the map for the first time. You can customize the cluster’s icon here.
  • onClusterUpdated
    Triggered when an existing cluster on the map is updated, such as when the number of markers in the cluster changes. This function allows you to refresh the cluster’s appearance accordingly.

After implementing these methods, we can proceed with additional cluster-related configurations by creating a helper class named MarkerManager.

class MarkerManager {

    private lateinit var clusterManager: CarClusterManager<CarClusterItem>
    private var googleMap: GoogleMap? = null

    fun initClusterManager(context: Context, map: GoogleMap) {
        clusterManager = CarClusterManager<CarClusterItem>(context, map)
        googleMap = map

        clusterManager.apply {
            setAlgorithm()
            renderer = CarClusterRenderer(context, map, this)
            map.setOnCameraIdleListener(this)
            setOnClusterClickListener { onClickedCluster(it, map) }
            setOnClusterItemClickListener { item ->
                onClickedClusterItem(item)
            }
        }
    }

    private fun setAlgorithm() {
        val metrics: DisplayMetrics = Resources.getSystem().displayMetrics
        val widthDp = (metrics.widthPixels / metrics.density).toInt()
        val heightDp = (metrics.heightPixels / metrics.density).toInt()
        clusterManager.algorithm = NonHierarchicalViewBasedAlgorithm(widthDp, heightDp)
    }

    private fun onClickedCluster(item: Cluster<CarClusterItem>, map: GoogleMap): Boolean {
        map.animateCamera(
            CameraUpdateFactory.newLatLngZoom(
                item.position,
                map.cameraPosition.zoom + 1
            )
        )
        return true
    }

    private fun onClickedClusterItem(
        item: CarClusterItem
    ): Boolean {
        val newItem = if (item.isSelected) {
            item.copy(isSelected = false)
        } else {
            item.copy(isSelected = true)
        }
        clusterManager.removeItem(item)
        clusterManager.addItem(newItem)
        clusterManager.cluster()
        googleMap?.animateCamera(
            CameraUpdateFactory.newLatLngZoom(
                newItem.position,
                googleMap?.cameraPosition?.zoom ?: 0f
            )
        )
        return true
    }

    fun addSampleMarkers() {
        for (i in 0..2000) {
            val lat = 40.989010 + (Math.random() - 0.5) / 10
            val lng = 29.022949 + (Math.random() - 0.5) / 10
            val carClusterItem = CarClusterItem(
                lat = lat,
                lng = lng,
                isSelected = false
            )
            clusterManager.addItem(carClusterItem)
            clusterManager.cluster()
        }
    }
}
  • initClusterManager()
    In this function, we create the ClusterManager and integrate it with the map. The ClusterManager optimizes performance by grouping markers shown on the map. Inside this function, we instantiate our custom CarClusterRenderer (which handles marker appearance) and assign it to the ClusterManager’s renderer property.
  • setAlgorithm()
    This function sets the algorithm that determines how the ClusterManager groups markers. Here, we use the NonHierarchicalViewBasedAlgorithm, which clusters markers based on the visible region of the screen. This algorithm only processes markers currently visible on the map, significantly improving performance. Being non-hierarchical, it dynamically manages markers to reduce unnecessary calculations and offers a smoother user experience.
  • onClickedCluster()
    This function handles the zoom-in action on the map when a clustered group is clicked.
  • onClickedClusterItem()
    This function triggers when an individual marker inside a cluster is clicked.
  • addSampleMarkers()
    This method creates sample markers and adds them to the map for demonstration purposes.

 

override fun onMapReady(googleMap: GoogleMap) {
    val istanbul = LatLng(40.989010, 29.022949)
    val lat = 40.989010 + (Math.random() - 0.5) / 10
    val lng = 29.022949 + (Math.random() - 0.5) / 10

    googleMap.moveCamera(
        CameraUpdateFactory.newLatLngZoom(
            istanbul,
            15f
        )
    )
    val markerManager = MarkerManager()

    markerManager.initClusterManager(context = this, map = googleMap)

    markerManager.addSampleMarkers()
}

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

Integrating ClusterManager in MainActivity

Finally, once our map is ready in the MainActivity, we create an instance of the MarkerManager class and call the initClusterManager() function. This initializes the clustering functionality and enables efficient marker management on the map.

I’ve shared the Cluster example in the GitHub repo. You can visit the repo to review the project in detail. Before running the project, you need to get your own Google Maps API key and add it to the project.

📱 Connect with me

This article was previously published on proandroiddev.com.

Menu