Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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.MapLibreMap
import org.maplibre.android.maps.Style

class ImageMarkerActivity : AppCompatActivity() {

    private lateinit var mapView: MapView
    private lateinit var map: MapLibreMap

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

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

            map.setStyle(
                Style.Builder().fromUri(
                    "https://gateway.mapmetrics-atlas.net/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.