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
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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])
)
}
}Symbol Layer with Custom Images (Recommended)
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
- Multiple Markers — Many markers with icons
- Custom Sprite — Sprite-based icons
- Animated Symbol Layer — Animated icons
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.