Реализация отображения маркеров на карте в мобильном приложении
Добавить один маркер — три строки кода. Отобразить 500 маркеров с кастомными иконками, корректным переиспользованием и тапами без фризов — это уже задача с нюансами.
Кастомные иконки: bitmap vs vector
Google Maps Android SDK принимает BitmapDescriptor, MapKit — ImageProvider, Mapbox — Drawable / UIImage. Рендерить bitmap из Canvas нужно один раз и кешировать — не в onMapReady для каждого маркера отдельно.
// Google Maps Android — кешированный BitmapDescriptor
private val markerCache = HashMap<String, BitmapDescriptor>()
fun getMarkerIcon(type: String): BitmapDescriptor {
return markerCache.getOrPut(type) {
val bitmap = Bitmap.createBitmap(48, 48, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = when (type) {
"cafe" -> Color.parseColor("#E74C3C")
"shop" -> Color.parseColor("#3498DB")
else -> Color.GRAY
}
}
canvas.drawCircle(24f, 24f, 20f, paint)
BitmapDescriptorFactory.fromBitmap(bitmap)
}
}
Создавать Bitmap каждый раз при добавлении маркера — прямой путь к OutOfMemoryError при 200+ объектах на экране.
Callout / InfoWindow при тапе
У Google Maps на Android InfoWindow рендерится как статичный снимок — внутри не работают кнопки и не обновляется динамический контент. Для интерактивного попапа используйте ViewAnnotation (Maps SDK v3+) или собственный FrameLayout поверх карты с позиционированием через Projection.toScreenLocation.
// iOS MapKit — кастомный callout через UIView
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let view = MKAnnotationView(annotation: annotation, reuseIdentifier: "custom")
view.image = UIImage(named: "pin")
view.canShowCallout = false // отключаем стандартный
// Кастомный callout добавляем в didSelect
return view
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
let callout = CustomCalloutView(annotation: view.annotation)
callout.center = CGPoint(x: view.bounds.midX, y: -callout.bounds.height / 2)
view.addSubview(callout)
}
Производительность: 500+ маркеров
При большом числе объектов нативные маркерные API начинают тормозить — каждый маркер это отдельный view. Порог зависит от устройства: на бюджетных Android заметные фризы появляются уже после 150-200 Marker объектов при одновременном добавлении.
Решение — GeoJSON-слой в Mapbox или TileOverlay в Google Maps: точки рендерятся как часть стиля карты, без создания объектов на каждую координату.
Для сценариев, где всё же нужны нативные маркеры с тапами — добавлять их частями через Handler.postDelayed или корутины:
lifecycleScope.launch {
locations.chunked(50).forEach { chunk ->
chunk.forEach { loc ->
googleMap.addMarker(
MarkerOptions()
.position(LatLng(loc.lat, loc.lng))
.icon(getMarkerIcon(loc.type))
)
}
delay(16) // один кадр, даём UI не зависнуть
}
}
Сроки
4 часа — 2 дня. Один тип маркеров с callout — полдня. Несколько типов с кешем иконок и интерактивными попапами — 1–2 дня. Стоимость рассчитывается индивидуально.







