Animate a Marker Along a Route
This tutorial shows how to smoothly animate a marker moving along a path — useful for showing vehicle tracking, delivery routes, or GPS playback.
Prerequisites
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Animate Marker with ValueAnimator
Move a symbol layer marker along a route using Android's animation framework:
kotlin
import android.animation.TypeEvaluator
import android.animation.ValueAnimator
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.animation.LinearInterpolator
import androidx.appcompat.app.AppCompatActivity
import com.google.gson.JsonObject
import org.maplibre.android.camera.CameraPosition
import org.maplibre.android.camera.CameraUpdateFactory
import org.maplibre.android.geometry.LatLng
import org.maplibre.android.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style
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.Point
class AnimateMarkerActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private lateinit var map: MapMetricsMap
private var markerAnimator: ValueAnimator? = null
// Route coordinates
private val routePoints = listOf(
LatLng(48.8566, 2.3522), // Paris
LatLng(48.8620, 2.3400),
LatLng(48.8680, 2.3310),
LatLng(48.8738, 2.2950), // Arc de Triomphe
LatLng(48.8750, 2.2870),
LatLng(48.8800, 2.2780),
LatLng(48.8867, 2.3431), // Sacré-Cœur
)
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 ->
setupMarker(style)
animateMarkerAlongRoute()
}
}
}
private fun setupMarker(style: Style) {
// Add marker icon
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_marker)
style.addImage("moving-marker", bitmap)
// Create GeoJSON source with initial position
val initialPoint = Point.fromLngLat(
routePoints[0].longitude,
routePoints[0].latitude
)
style.addSource(
GeoJsonSource("marker-source", Feature.fromGeometry(initialPoint))
)
// Add symbol layer
style.addLayer(
SymbolLayer("marker-layer", "marker-source")
.withProperties(
iconImage("moving-marker"),
iconSize(1.0f),
iconAllowOverlap(true),
iconIgnorePlacement(true)
)
)
// Set camera
map.cameraPosition = CameraPosition.Builder()
.target(routePoints[0])
.zoom(13.0)
.build()
}
private fun animateMarkerAlongRoute() {
// Animate through each segment
animateSegment(0)
}
private fun animateSegment(index: Int) {
if (index >= routePoints.size - 1) return
val start = routePoints[index]
val end = routePoints[index + 1]
markerAnimator = ValueAnimator.ofObject(
LatLngEvaluator(), start, end
).apply {
duration = 2000 // 2 seconds per segment
interpolator = LinearInterpolator()
addUpdateListener { animation ->
val position = animation.animatedValue as LatLng
val point = Point.fromLngLat(position.longitude, position.latitude)
// Update marker position
val source = map.style?.getSource("marker-source") as? GeoJsonSource
source?.setGeoJson(Feature.fromGeometry(point))
// Follow marker with camera
map.easeCamera(
CameraUpdateFactory.newLatLng(position),
100
)
}
addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
// Move to next segment
animateSegment(index + 1)
}
})
start()
}
}
// Custom evaluator for LatLng interpolation
private class LatLngEvaluator : TypeEvaluator<LatLng> {
override fun evaluate(fraction: Float, startValue: LatLng, endValue: LatLng): LatLng {
val lat = startValue.latitude + (endValue.latitude - startValue.latitude) * fraction
val lng = startValue.longitude + (endValue.longitude - startValue.longitude) * fraction
return LatLng(lat, lng)
}
}
override fun onDestroy() {
markerAnimator?.cancel()
super.onDestroy()
mapView.onDestroy()
}
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 onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
}
}With Route Trail Line
Show the path behind the marker as it moves:
kotlin
import org.maplibre.android.style.layers.LineLayer
import org.maplibre.geojson.FeatureCollection
import org.maplibre.geojson.LineString
private val visitedPoints = mutableListOf<Point>()
private fun setupRouteTrail(style: Style) {
// Trail source — starts empty
style.addSource(GeoJsonSource("trail-source"))
// Trail line layer
style.addLayer(
LineLayer("trail-layer", "trail-source")
.withProperties(
lineColor("#4285F4"),
lineWidth(3f),
lineOpacity(0.7f)
)
)
}
private fun updateTrail(position: LatLng) {
visitedPoints.add(
Point.fromLngLat(position.longitude, position.latitude)
)
if (visitedPoints.size >= 2) {
val lineString = LineString.fromLngLats(visitedPoints)
val source = map.style?.getSource("trail-source") as? GeoJsonSource
source?.setGeoJson(Feature.fromGeometry(lineString))
}
}Then call updateTrail(position) inside the addUpdateListener callback.
With Play/Pause Controls
kotlin
import android.widget.Button
private var isPlaying = true
private fun setupControls() {
val playPauseBtn = findViewById<Button>(R.id.btnPlayPause)
playPauseBtn.setOnClickListener {
if (isPlaying) {
markerAnimator?.pause()
playPauseBtn.text = "Play"
} else {
markerAnimator?.resume()
playPauseBtn.text = "Pause"
}
isPlaying = !isPlaying
}
findViewById<Button>(R.id.btnRestart).setOnClickListener {
markerAnimator?.cancel()
visitedPoints.clear()
animateSegment(0)
isPlaying = true
playPauseBtn.text = "Pause"
}
}Next Steps
- Animated Symbol Layer — Advanced symbol animations
- Fly to a Location — Camera flight animations
- Polyline Route — Static route drawing
Tip: For smoother animation at high zoom levels, interpolate every 10-20ms. For performance with many animated markers, use a single GeoJSON source with multiple features instead of separate sources per marker.