Реализация кластеризации маркеров на карте мобильного приложения

TRUETECH занимается разработкой, поддержкой и обслуживанием мобильных приложений iOS, Android, PWA. Имеем большой опыт и экспертизу для публикации мобильных приложений в популярные маркеты Google Play, App Store, Amazon, AppGallery и другие.

Разработка и поддержка любых видов мобильных приложений:

Информационные и развлекательные мобильные приложения
Новостные приложения, игры, справочники, онлайн-каталоги, погодные, фитнес и здоровье, туристические, образовательные, социальные сети и мессенджеры, квиз, блоги и подкасты, форумы, агрегаторы
Мобильные приложения электронной коммерции
Интернет-магазины, B2B-приложения, маркетплейсы, онлайн-обменники, кэшбэк-сервисы, биржи, дропшиппинг-платформы, программы лояльности, доставка еды и товаров, платежные системы
Мобильные приложения для управления бизнес-процессами
CRM-системы, ERP-системы, управление проектами, инструменты для команды продаж, учет финансов, управление производством, логистика и доставка, управление персоналом, системы мониторинга данных
Мобильные приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, платформы предоставления электронных услуг, платформы кешбека, видеохостинги, тематические порталы, платформы онлайн-бронирования и записи, платформы онлайн-торговли

Это лишь некоторые из типы мобильных приложений, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента.

Услуги, которые мы предлагаем
Показано 1 из 1Все 1735 услуг
Реализация кластеризации маркеров на карте мобильного приложения
Средний
от 1 дня до 3 дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_mobile-applications_feedme_467_0.webp
    Разработка мобильного приложения для компании FEEDME
    792
  • image_mobile-applications_xoomer_471_0.webp
    Разработка мобильного приложения для компании XOOMER
    671
  • image_mobile-applications_rhl_428_0.webp
    Разработка мобильного приложения для компании RHL
    1097
  • image_mobile-applications_zippy_411_0.webp
    Разработка мобильного приложения для компании ZIPPY
    969
  • image_mobile-applications_affhome_429_0.webp
    Разработка мобильного приложения для компании Affhome
    914
  • image_mobile-applications_flavors_409_0.webp
    Разработка мобильного приложения для компании FLAVORS
    495

Реализация кластеризации маркеров на карте мобильного приложения

Карта с 300 маркерами без кластеризации — это нечитаемое месиво иконок и лаг при зуме. Кластеризация группирует близлежащие точки в один объект с числом, которое раскрывается при приближении. Реализация зависит от выбранного картографического SDK.

Google Maps: Maps SDK Clustering Utility

Для Google Maps используется библиотека maps-utils:

// 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)

        // Кастомный рендерер
        clusterManager.renderer = CustomClusterRenderer(this, googleMap, clusterManager)

        // Добавление точек
        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
}

Кастомный вид кластера

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 (Яндекс): ClusterizedPlacemarkCollection

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

// Добавление точек
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 — радиус в пикселях для объединения. minZoom — начиная с какого уровня зума точки показываются по отдельности.

Mapbox: встроенная кластеризация через GeoJSON

Mapbox поддерживает кластеризацию на уровне источника данных — всё происходит на стороне рендерера без дополнительных библиотек:

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

// Слой кластеров
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) }
            )
        )
    }
)

// Слой с числом внутри кластера
style.addLayer(
    SymbolLayer("cluster-count", "locations").apply {
        filter(has("point_count"))
        textField(get("point_count_abbreviated"))
        textSize(12.0)
        textColor(Color.WHITE)
    }
)

// Слой одиночных точек
style.addLayer(
    SymbolLayer("unclustered-point", "locations").apply {
        filter(not(has("point_count")))
        iconImage("custom-pin")
    }
)

Подход через GeoJSON-источник масштабируется до десятков тысяч точек без заметного падения FPS — рендеринг полностью на GPU.

Тап по кластеру: zoom to bounds

При клике по кластеру карта должна приближаться так, чтобы все точки кластера вошли в экран:

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
}

Сроки

1–3 дня. Стандартная кластеризация — 1 день. Кастомный вид кластеров + анимация раскрытия + тап с зумом — 2–3 дня. Стоимость рассчитывается индивидуально.