Map marker clustering in mobile app

NOVASOLUTIONS.TECHNOLOGY is engaged in the development, support and maintenance of iOS, Android, PWA mobile applications. We have extensive experience and expertise in publishing mobile applications in popular markets like Google Play, App Store, Amazon, AppGallery and others.
Development and support of all types of mobile applications:
Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1 servicesAll 1735 services
Map marker clustering in mobile app
Medium
from 1 business day to 3 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    756
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1052
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    862
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    445

Implementation of Map Marker Clustering in a Mobile Application

A map with 300 markers without clustering is an unreadable mess of icons and lag on zoom. Clustering groups nearby points into a single object with a count that expands when zoomed in. Implementation depends on the chosen map SDK.

Google Maps: Maps SDK Clustering Utility

For Google Maps, use the maps-utils library:

// build.gradle
implementation("com.google.maps.android:android-maps-utils:3.8.2")

class ClusteringActivity : AppCompatActivity(), OnMapReadyCallback {
    private lateinit var clusterManager: ClusterManager<MyClusterItem>

    override fun onMapReady(googleMap: GoogleMap) {
        clusterManager = ClusterManager(this, googleMap)
        googleMap.setOnCameraIdleListener(clusterManager)
        googleMap.setOnMarkerClickListener(clusterManager)

        // Custom renderer
        clusterManager.renderer = CustomClusterRenderer(this, googleMap, clusterManager)

        // Add points
        val items = locations.map { MyClusterItem(it.lat, it.lng, it.title) }
        clusterManager.addItems(items)
        clusterManager.cluster()
    }
}

data class MyClusterItem(
    private val lat: Double,
    private val lng: Double,
    private val title: String
) : ClusterItem {
    override fun getPosition() = LatLng(lat, lng)
    override fun getTitle() = title
    override fun getSnippet() = null
    override fun getZIndex() = 0f
}

Custom Cluster Appearance

class CustomClusterRenderer(
    context: Context,
    map: GoogleMap,
    clusterManager: ClusterManager<MyClusterItem>
) : DefaultClusterRenderer<MyClusterItem>(context, map, clusterManager) {

    override fun onBeforeClusterItemRendered(item: MyClusterItem, markerOptions: MarkerOptions) {
        markerOptions.icon(BitmapDescriptorFactory.fromResource(R.drawable.custom_pin))
    }

    override fun onBeforeClusterRendered(
        cluster: Cluster<MyClusterItem>,
        markerOptions: MarkerOptions
    ) {
        val count = cluster.size
        val bitmap = createClusterBitmap(count)
        markerOptions.icon(BitmapDescriptorFactory.fromBitmap(bitmap))
    }

    private fun createClusterBitmap(count: Int): Bitmap {
        val size = 80
        val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(bitmap)
        val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.parseColor("#3498DB") }
        canvas.drawCircle(size / 2f, size / 2f, size / 2f - 2f, bgPaint)
        val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
            color = Color.WHITE
            textSize = 28f
            textAlign = Paint.Align.CENTER
        }
        canvas.drawText(count.toString(), size / 2f, size / 2f + 10f, textPaint)
        return bitmap
    }
}

MapKit (Yandex): ClusterizedPlacemarkCollection

val clusterizedCollection = map.mapObjects.addClusterizedPlacemarkCollection(
    object : ClusterListener {
        override fun onClustersAdded(clusters: MutableCollection<Cluster>) {
            clusters.forEach { cluster ->
                cluster.appearance.setIcon(
                    TextImageProvider(buildClusterImage(cluster.size))
                )
            }
        }
    }
)

// Add points
val placemarks = locations.map { Point(it.lat, it.lng) }
val options = PlacemarkCreationContext()
clusterizedCollection.addPlacemarks(placemarks, ImageProvider.fromResource(context, R.drawable.pin), options)
clusterizedCollection.clusterPlacemarks(clusterRadius = 60.0, minZoom = 15)

clusterRadius — radius in pixels for grouping. minZoom — starting from which zoom level points are shown separately.

Mapbox: Built-in Clustering via GeoJSON

Mapbox supports clustering at the data source level — everything happens on the renderer side without additional libraries:

style.addSource(
    GeoJsonSource.Builder("locations")
        .featureCollection(featureCollection)
        .cluster(true)
        .clusterMaxZoom(14)
        .clusterRadius(50)
        .build()
)

// Cluster layer
style.addLayer(
    CircleLayer("clusters", "locations").apply {
        filter(has("point_count"))
        circleColor(
            step(get("point_count"),
                color(Color.parseColor("#51bbd6")),
                stop { literal(100); color(Color.parseColor("#f1f075")) },
                stop { literal(750); color(Color.parseColor("#f28cb1")) }
            )
        )
        circleRadius(
            step(get("point_count"),
                literal(20),
                stop { literal(100); literal(30) },
                stop { literal(750); literal(40) }
            )
        )
    }
)

// Layer with cluster count
style.addLayer(
    SymbolLayer("cluster-count", "locations").apply {
        filter(has("point_count"))
        textField(get("point_count_abbreviated"))
        textSize(12.0)
        textColor(Color.WHITE)
    }
)

// Layer for individual points
style.addLayer(
    SymbolLayer("unclustered-point", "locations").apply {
        filter(not(has("point_count")))
        iconImage("custom-pin")
    }
)

The GeoJSON source approach scales to tens of thousands of points without noticeable FPS drops — rendering is entirely on GPU.

Tap on Cluster: Zoom to Bounds

When clicking a cluster, the map should zoom so all cluster points fit on screen:

mapView.mapboxMap.addOnMapClickListener { point ->
    val features = mapView.mapboxMap.queryRenderedFeatures(
        ScreenCoordinate(point.x, point.y),
        RenderedQueryOptions(listOf("clusters"), null)
    )
    features.value?.firstOrNull()?.let { feature ->
        val clusterId = feature.id()?.toLongOrNull() ?: return@addOnMapClickListener true
        (style.getSource("locations") as? GeoJsonSource)?.getClusterExpansionZoom(
            Feature.fromGeometry(feature.geometry()!!)
        ) { result ->
            result.value?.let { zoom ->
                mapView.mapboxMap.setCamera(
                    CameraOptions.Builder()
                        .center(feature.geometry() as? Point)
                        .zoom(zoom.toDouble() + 0.5)
                        .build()
                )
            }
        }
    }
    true
}

Timeline

1–3 days. Standard clustering — 1 day. Custom cluster appearance + expansion animation + tap with zoom — 2–3 days. Cost is calculated individually.