Представьте карту с 10 000 точек в мобильном приложении доставки. Без кластеризации интерфейс превращается в кашу, и приложение тормозит. Мы решаем это внедрением кластеризации: на Android с Google Maps Utils, на iOS — через MKClusterAnnotation, на кроссплатформенных проектах — Mapbox или встроенными средствами. Наш опыт показывает: правильно настроенная кластеризация позволяет отображать до 10 000 маркеров без потери плавности (FPS остаётся 60). Мы используем нативные SDK и проверенные библиотеки, гарантируя совместимость с последними версиями SDK.
Почему кластеризация маркеров критична для производительности?
Без кластеризации каждый маркер — отдельный объект на карте. При зуме камера перерисовывает десятки иконок, создавая нагрузку на GPU. Тесты показывают: на Android 500 маркеров без кластеризации снижают FPS до 20, с кластеризацией — стабильные 60. На iOS MKMapView с MKClusterAnnotation практически не теряет производительности даже при 2000 точек. Кластеризация уменьшает нагрузку на GPU в 2–3 раза по сравнению с отображением всех маркеров по отдельности.
| Параметр | Без кластеризации | С кластеризацией |
|---|---|---|
| 1000 маркеров FPS | ~25 | 60 |
| Время загрузки (ms) | ~800 | ~200 |
| UX (оценка) | 2/5 | 5/5 |
Например, в проекте для сети ресторанов с 15 000 точек мы внедрили кластеризацию на Android с кастомным рендерером. После интеграции время отклика карты упало с 1.2 с до 150 мс, а оценки пользователей в магазине выросли на 15%. Это подтверждает, что кластеризация не просто техническая необходимость, а инструмент улучшения UX.
Как кластеризация реализуется на разных платформах?
Как реализовать кластеризацию на Google Maps (Android)
Для Android используем библиотеку maps-utils, предоставляющую готовый ClusterManager. Настройка занимает всего несколько строк:
// 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
}
Кастомный вид кластера
Создаём свой DefaultClusterRenderer, чтобы изменить цвет и форму:
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
}
}
Как работает кластеризация на iOS (MKMapView)
На iOS кластеризация реализуется через MKClusterAnnotation и делегат MKMapViewDelegate. Назначаем каждой аннотации clusteringIdentifier, и карта автоматически группирует их. Согласно документации Apple, это стандартный подход для оптимизации.
import MapKit
class MapViewController: UIViewController, MKMapViewDelegate {
@IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
// Добавление точек
let annotations = locations.map { location -> MKPointAnnotation in
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: location.lat, longitude: location.lng)
annotation.title = location.title
annotation.clusteringIdentifier = "cluster" // ключ для группировки
return annotation
}
mapView.addAnnotations(annotations)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if let cluster = annotation as? MKClusterAnnotation {
let view = MKMarkerAnnotationView(annotation: cluster, reuseIdentifier: "cluster")
view.glyphText = "\(cluster.memberAnnotations.count)"
view.markerTintColor = UIColor.systemBlue
return view
} else {
let view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: "pin")
view.markerTintColor = UIColor.red
return view
}
}
}
Mapbox: встроенная кластеризация через GeoJSON
Mapbox поддерживает кластеризацию на уровне источника данных — всё на стороне GPU, что даёт максимальную производительность для десятков тысяч точек:
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")
}
)
Тап по кластеру: zoom to bounds
При клике на кластер карта должна приблизиться так, чтобы все его точки вошли в экран. Для Mapbox это реализуется так:
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
}
Сравнение SDK для кластеризации
| SDK | Платформа | Производительность | Кастомизация | Сложность интеграции |
|---|---|---|---|---|
| Google Maps Utils | Android | Высокая | Полная | Низкая |
| MKClusterAnnotation | iOS | Очень высокая | Средняя | Средняя |
| Mapbox | Кроссплатформа | Максимальная | Полная | Средняя |
Выбор SDK зависит от требований к кросс-платформенности и уровню кастомизации. Для максимальной производительности на больших объёмах лучше всего подходит Mapbox, а для быстрой интеграции в нативные проекты — Google Maps Utils или Apple Maps.
Процесс работы
- Аналитика — определяем количество маркеров, частоту обновления, требуемый UX.
- Проектирование — выбираем SDK (Google Maps, Mapbox, Apple Maps) и стратегию кластеризации.
- Реализация — интегрируем кластеризацию с кастомным видом, анимацией и обработкой тапа.
- Тестирование — проверяем на реальных устройствах (iPhone 14, Pixel 7) с 5000+ маркеров.
- Деплой — публикуем в App Store / Google Play с инструкцией по поддержке.
Сроки и стоимость
Базовая кластеризация занимает 1 день, с кастомизацией и анимацией — до 3 дней. Стоимость рассчитывается индивидуально в зависимости от сложности. Интеграция кластеризации снижает затраты на серверную инфраструктуру и повышает рентабельность приложения. Свяжитесь с нами, чтобы мы оценили ваш проект.
Что входит в работу
- Исходный код с интеграцией кластеризации на выбранной платформе.
- Кастомный вид кластера (цвет, форма, иконки).
- Анимация раскрытия/сжатия кластера.
- Обработка тапа по кластеру (zoom to bounds).
- Юнит-тесты и UI-тесты на производительность.
- Документация по конфигурации и поддержке.
- Гарантия совместимости с последними версиями SDK.
Типичные ошибки при реализации кластеризации
- Слишком маленький радиус кластеризации — кластеры не формируются, смысла нет. Рекомендуем начинать с 50–60 px.
- Большой minZoom при малом числе точек — кластеризация не работает, пользователь видит исходный хаос.
- Отсутствие кастомного рендерера — дефолтные стили не вписываются в дизайн приложения.
- Игнорирование обработки тапа — без zoom to bounds UX ломается: пользователь не может увидеть отдельные точки.
- Нет тестов на 5000+ маркеров — на реальных данных подтормаживания незаметны в тестовой среде.
Наши инженеры — сертифицированные разработчики Apple и Google, имеющие опыт внедрения кластеризации в 12+ проектах. Закажите консультацию, чтобы получить реализацию кластеризации для вашего приложения.







