Skip to content

Add Custom Image Markers

This tutorial shows how to use custom images (PNG, SVG, drawable) as marker icons on your MapMetrics Android map instead of the default pin.

Prerequisites

Custom Icon from Drawable Resource

Use an Android drawable as a marker icon:

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.CameraPosition
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style

class ImageMarkerActivity : AppCompatActivity() {

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

    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 ->
                addCustomMarkers()
            }
        }
    }

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

        // Convert vector drawable to bitmap
        val icon = iconFactory.fromBitmap(
            drawableToBitmap(R.drawable.ic_custom_pin)
        )

        map.addMarker(
            MarkerOptions()
                .position(LatLng(48.8584, 2.2945))
                .title("Eiffel Tower")
                .icon(icon)
        )

        map.cameraPosition = CameraPosition.Builder()
            .target(LatLng(48.8584, 2.2945))
            .zoom(14.0)
            .build()
    }

    private fun drawableToBitmap(drawableRes: Int): 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)
        return bitmap
    }

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

Different Icons per Category

Assign different icons based on data:

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

    val icons = mapOf(
        "restaurant" to iconFactory.fromBitmap(drawableToBitmap(R.drawable.ic_restaurant)),
        "museum" to iconFactory.fromBitmap(drawableToBitmap(R.drawable.ic_museum)),
        "park" to iconFactory.fromBitmap(drawableToBitmap(R.drawable.ic_park)),
        "hotel" to iconFactory.fromBitmap(drawableToBitmap(R.drawable.ic_hotel)),
    )

    val places = listOf(
        Triple(LatLng(48.8584, 2.2945), "Café de Flore", "restaurant"),
        Triple(LatLng(48.8606, 2.3376), "Louvre Museum", "museum"),
        Triple(LatLng(48.8462, 2.3464), "Luxembourg Gardens", "park"),
        Triple(LatLng(48.8680, 2.3280), "Le Meurice", "hotel"),
    )

    for ((position, name, category) in places) {
        map.addMarker(
            MarkerOptions()
                .position(position)
                .title(name)
                .snippet(category.replaceFirstChar { it.uppercase() })
                .icon(icons[category])
        )
    }
}

For many markers, use a symbol layer for better performance:

kotlin
import android.graphics.BitmapFactory
import com.google.gson.JsonObject
import org.maplibre.android.style.expressions.Expression.*
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) {
    // Add images to the style
    style.addImage("pin-red", drawableToBitmap(R.drawable.ic_pin_red))
    style.addImage("pin-blue", drawableToBitmap(R.drawable.ic_pin_blue))
    style.addImage("pin-green", drawableToBitmap(R.drawable.ic_pin_green))

    // Create features with an "icon" property
    val features = listOf(
        createFeature(2.2945, 48.8584, "Eiffel Tower", "pin-red"),
        createFeature(2.3376, 48.8606, "Louvre", "pin-blue"),
        createFeature(2.3464, 48.8462, "Luxembourg", "pin-green"),
    )

    style.addSource(
        GeoJsonSource("markers-source", FeatureCollection.fromFeatures(features))
    )

    // Symbol layer — picks icon based on feature property
    style.addLayer(
        SymbolLayer("markers-layer", "markers-source")
            .withProperties(
                iconImage(get("icon")),   // Use the "icon" property
                iconSize(0.8f),
                iconAllowOverlap(true),
                iconAnchor("bottom"),     // Anchor at bottom of icon
                textField(get("name")),
                textSize(12f),
                textOffset(arrayOf(0f, 0.8f)),
                textAnchor("top"),
                textColor("#333333"),
                textHaloColor("#ffffff"),
                textHaloWidth(1f)
            )
    )
}

private fun createFeature(lng: Double, lat: Double, name: String, icon: String): Feature {
    val props = JsonObject().apply {
        addProperty("name", name)
        addProperty("icon", icon)
    }
    return Feature.fromGeometry(Point.fromLngLat(lng, lat), props)
}

Scaled Marker from Bitmap

Resize a bitmap for consistent marker size:

kotlin
private fun scaledBitmap(drawableRes: Int, widthDp: Int, heightDp: Int): Bitmap {
    val density = resources.displayMetrics.density
    val widthPx = (widthDp * density).toInt()
    val heightPx = (heightDp * density).toInt()

    val original = drawableToBitmap(drawableRes)
    return Bitmap.createScaledBitmap(original, widthPx, heightPx, true)
}

// Usage:
val icon = iconFactory.fromBitmap(scaledBitmap(R.drawable.ic_pin, 32, 48))

Next Steps


Tip: For the symbol layer approach, use iconAnchor("bottom") for pin-style icons so the tip of the pin aligns with the geographic coordinate. The default anchor is center, which causes pins to appear to float above their position.