Skip to content

Add Multiple Markers with Custom Icons

This tutorial shows how to add many markers with custom icons, categories, and info windows to your MapMetrics Android map.

Prerequisites

Multiple Markers from a List

Add markers from a data list with titles and snippets:

kotlin
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import org.maplibre.android.annotations.IconFactory
import org.maplibre.android.annotations.MarkerOptions
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.geometry.LatLngBounds
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style

class MultipleMarkersActivity : AppCompatActivity() {

    private lateinit var mapView: MapView
    private lateinit var map: MapMetricsMap

    data class Place(
        val position: LatLng,
        val name: String,
        val description: String,
        val category: String
    )

    private val places = listOf(
        Place(LatLng(48.8584, 2.2945), "Eiffel Tower", "Iconic iron tower, 324m tall", "landmark"),
        Place(LatLng(48.8606, 2.3376), "Louvre Museum", "World's largest art museum", "museum"),
        Place(LatLng(48.8530, 2.3499), "Notre-Dame", "Medieval Catholic cathedral", "landmark"),
        Place(LatLng(48.8738, 2.2950), "Arc de Triomphe", "Triumphal arch monument", "landmark"),
        Place(LatLng(48.8462, 2.3464), "Luxembourg Gardens", "Historic public garden", "park"),
        Place(LatLng(48.8600, 2.3266), "Musée d'Orsay", "Impressionist art museum", "museum"),
        Place(LatLng(48.8867, 2.3431), "Sacré-Cœur", "White-domed basilica", "landmark"),
        Place(LatLng(48.8619, 2.3532), "Centre Pompidou", "Modern art museum", "museum"),
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_map)

        mapView = findViewById(R.id.mapView)
        mapView.onCreate(savedInstanceState)
        mapView.getMapAsync { mapMetricsMap ->
            map = mapMetricsMap

            map.setStyle(
                Style.Builder().fromUri(
                    "https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY"
                )
            ) { style ->
                addMarkers()
            }
        }
    }

    private fun addMarkers() {
        val boundsBuilder = LatLngBounds.Builder()

        for (place in places) {
            map.addMarker(
                MarkerOptions()
                    .position(place.position)
                    .title(place.name)
                    .snippet("${place.category.uppercase()} — ${place.description}")
            )
            boundsBuilder.include(place.position)
        }

        // Fit all markers
        map.easeCamera(
            CameraUpdateFactory.newLatLngBounds(boundsBuilder.build(), 80),
            1000
        )
    }

    override fun onStart() { super.onStart(); mapView.onStart() }
    override fun onResume() { super.onResume(); mapView.onResume() }
    override fun onPause() { super.onPause(); mapView.onPause() }
    override fun onStop() { super.onStop(); mapView.onStop() }
    override fun onDestroy() { super.onDestroy(); mapView.onDestroy() }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }
}

Markers with Custom Vector Icons

Use Android drawable resources as marker icons:

kotlin
private fun addMarkersWithIcons() {
    val iconFactory = IconFactory.getInstance(this)

    for (place in places) {
        // Pick icon drawable based on category
        val drawableRes = when (place.category) {
            "landmark" -> R.drawable.ic_landmark
            "museum" -> R.drawable.ic_museum
            "park" -> R.drawable.ic_park
            else -> R.drawable.ic_default_marker
        }

        // Convert vector drawable to bitmap
        val drawable = ContextCompat.getDrawable(this, drawableRes)!!
        val bitmap = Bitmap.createBitmap(
            drawable.intrinsicWidth,
            drawable.intrinsicHeight,
            Bitmap.Config.ARGB_8888
        )
        val canvas = Canvas(bitmap)
        drawable.setBounds(0, 0, canvas.width, canvas.height)
        drawable.draw(canvas)

        val icon = iconFactory.fromBitmap(bitmap)

        map.addMarker(
            MarkerOptions()
                .position(place.position)
                .title(place.name)
                .snippet(place.description)
                .icon(icon)
        )
    }
}

For 50+ markers, use a symbol layer instead of annotation markers for better performance:

kotlin
import com.google.gson.JsonObject
import org.maplibre.android.style.layers.PropertyFactory.*
import org.maplibre.android.style.layers.SymbolLayer
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.Point

private fun addSymbolLayerMarkers(style: Style) {
    // Build feature collection from places
    val features = places.map { place ->
        val properties = JsonObject().apply {
            addProperty("name", place.name)
            addProperty("description", place.description)
            addProperty("category", place.category)
        }
        Feature.fromGeometry(
            Point.fromLngLat(
                place.position.longitude,
                place.position.latitude
            ),
            properties
        )
    }

    val featureCollection = FeatureCollection.fromFeatures(features)

    // Add source
    style.addSource(GeoJsonSource("places-source", featureCollection))

    // Add symbol layer
    style.addLayer(
        SymbolLayer("places-layer", "places-source")
            .withProperties(
                iconImage("marker-15"),       // Built-in sprite icon
                iconSize(1.5f),
                iconAllowOverlap(true),
                textField(Expression.get("name")),
                textSize(11f),
                textOffset(arrayOf(0f, 1.5f)),
                textAnchor("top"),
                textColor("#333333"),
                textHaloColor("#ffffff"),
                textHaloWidth(1f)
            )
    )

    // Handle tap on symbols
    map.addOnMapClickListener { latLng ->
        val screenPoint = map.projection.toScreenLocation(latLng)
        val features = map.queryRenderedFeatures(screenPoint, "places-layer")

        if (features.isNotEmpty()) {
            val name = features[0].getStringProperty("name")
            val desc = features[0].getStringProperty("description")
            android.widget.Toast.makeText(
                this, "$name: $desc", android.widget.Toast.LENGTH_SHORT
            ).show()
        }
        true
    }
}

Filter Markers by Category

Show/hide markers by category with buttons:

kotlin
import android.widget.ToggleButton
import org.maplibre.android.style.expressions.Expression
import org.maplibre.android.style.expressions.Expression.*

private fun setupCategoryFilters(style: Style) {
    addSymbolLayerMarkers(style) // Add symbol layer first

    val layer = style.getLayer("places-layer") as? SymbolLayer

    findViewById<ToggleButton>(R.id.btnLandmarks).setOnCheckedChangeListener { _, checked ->
        updateFilter(layer)
    }
    findViewById<ToggleButton>(R.id.btnMuseums).setOnCheckedChangeListener { _, checked ->
        updateFilter(layer)
    }
    findViewById<ToggleButton>(R.id.btnParks).setOnCheckedChangeListener { _, checked ->
        updateFilter(layer)
    }
}

private fun updateFilter(layer: SymbolLayer?) {
    val activeCategories = mutableListOf<Expression>()

    if (findViewById<ToggleButton>(R.id.btnLandmarks).isChecked) {
        activeCategories.add(literal("landmark"))
    }
    if (findViewById<ToggleButton>(R.id.btnMuseums).isChecked) {
        activeCategories.add(literal("museum"))
    }
    if (findViewById<ToggleButton>(R.id.btnParks).isChecked) {
        activeCategories.add(literal("park"))
    }

    if (activeCategories.isEmpty()) {
        // Show all when nothing selected
        layer?.setFilter(literal(true))
    } else {
        layer?.setFilter(
            match(
                get("category"),
                literal(false),
                *activeCategories.map { stop(it, literal(true)) }.toTypedArray()
            )
        )
    }
}

Annotation vs Symbol Layer

FeatureAnnotation MarkersSymbol Layer
PerformanceGood for < 50Scales to thousands
Custom viewsFull Android viewsIcons + text only
Built-in info windowYesManual via click query
Drag supportYesNo
Data-driven stylingNoYes (expressions)
FilteringRemove/add manuallysetFilter() on layer

Next Steps


Tip: Switch from annotation markers to symbol layers when you have more than ~50 markers. Symbol layers are GPU-rendered and handle thousands of points smoothly, while annotation markers create individual Android views that can cause jank at scale.