Animate a Line Being Drawn
This tutorial shows how to animate a line being progressively drawn on your MapMetrics Android map — great for showing routes, delivery paths, or GPS tracks being traced in real time.
Prerequisites
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Progressive Line Animation
Draw a line point by point with smooth animation:
kotlin
import android.animation.ValueAnimator
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.animation.LinearInterpolator
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import org.maplibre.android.camera.CameraPosition
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
import org.maplibre.android.style.layers.LineLayer
import org.maplibre.android.style.layers.PropertyFactory.*
import org.maplibre.android.style.sources.GeoJsonSource
import org.maplibre.geojson.Feature
import org.maplibre.geojson.LineString
import org.maplibre.geojson.Point
class AnimateLineActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private lateinit var map: MapMetricsMap
private val routePoints = listOf(
Point.fromLngLat(2.3522, 48.8566), // Paris
Point.fromLngLat(2.3700, 48.8550),
Point.fromLngLat(2.3800, 48.8600),
Point.fromLngLat(2.3900, 48.8580),
Point.fromLngLat(2.4000, 48.8620),
Point.fromLngLat(2.4100, 48.8590),
Point.fromLngLat(2.4200, 48.8640),
Point.fromLngLat(2.4300, 48.8610),
)
private var currentPointIndex = 0
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_animate_line)
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 ->
setupLine(style)
// Fit camera to full route
val bounds = LatLngBounds.Builder()
for (point in routePoints) {
bounds.include(LatLng(point.latitude(), point.longitude()))
}
map.easeCamera(
CameraUpdateFactory.newLatLngBounds(bounds.build(), 80),
1000
)
}
// Start animation button
findViewById<Button>(R.id.btnStart).setOnClickListener {
currentPointIndex = 0
startLineAnimation()
}
}
}
private fun setupLine(style: Style) {
// Empty source for the animated line
style.addSource(GeoJsonSource("line-source"))
// Line layer
style.addLayer(
LineLayer("line-layer", "line-source")
.withProperties(
lineColor(Color.parseColor("#4285F4")),
lineWidth(4f),
lineOpacity(0.9f),
lineJoin("round"),
lineCap("round")
)
)
}
private fun startLineAnimation() {
currentPointIndex = 1
val drawNextSegment = object : Runnable {
override fun run() {
if (currentPointIndex >= routePoints.size) return
// Update line with points up to current index
val visiblePoints = routePoints.subList(0, currentPointIndex + 1)
val lineString = LineString.fromLngLats(visiblePoints)
val source = map.style?.getSource("line-source") as? GeoJsonSource
source?.setGeoJson(Feature.fromGeometry(lineString))
currentPointIndex++
if (currentPointIndex < routePoints.size) {
handler.postDelayed(this, 500) // 500ms between points
}
}
}
handler.post(drawNextSegment)
}
override fun onDestroy() {
handler.removeCallbacksAndMessages(null)
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)
}
}Smooth Interpolated Line Animation
Interpolate smoothly between points using ValueAnimator:
kotlin
private fun startSmoothAnimation() {
animateSegment(0)
}
private fun animateSegment(index: Int) {
if (index >= routePoints.size - 1) return
val start = routePoints[index]
val end = routePoints[index + 1]
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000 // 1 second per segment
interpolator = LinearInterpolator()
addUpdateListener { animation ->
val fraction = animation.animatedValue as Float
// Interpolate between start and end
val currentLng = start.longitude() + (end.longitude() - start.longitude()) * fraction
val currentLat = start.latitude() + (end.latitude() - start.latitude()) * fraction
// Build line from first point to current interpolated position
val visiblePoints = routePoints.subList(0, index + 1).toMutableList()
visiblePoints.add(Point.fromLngLat(currentLng, currentLat))
val lineString = LineString.fromLngLats(visiblePoints)
val source = map.style?.getSource("line-source") as? GeoJsonSource
source?.setGeoJson(Feature.fromGeometry(lineString))
}
addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
animateSegment(index + 1) // Next segment
}
})
start()
}
}With Trailing Marker
Show a dot at the leading edge of the animated line:
kotlin
import android.graphics.BitmapFactory
import org.maplibre.android.style.layers.SymbolLayer
private fun setupLeadingMarker(style: Style) {
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.ic_dot)
style.addImage("dot-icon", bitmap)
style.addSource(GeoJsonSource("dot-source",
Feature.fromGeometry(routePoints[0])
))
style.addLayer(
SymbolLayer("dot-layer", "dot-source")
.withProperties(
iconImage("dot-icon"),
iconSize(0.8f),
iconAllowOverlap(true)
)
)
}
// Inside your animation update listener, also update the dot:
private fun updateDot(lat: Double, lng: Double) {
val source = map.style?.getSource("dot-source") as? GeoJsonSource
source?.setGeoJson(Feature.fromGeometry(Point.fromLngLat(lng, lat)))
}Next Steps
- Animate a Marker — Move markers along paths
- Polyline Route — Static route lines
- Fly to a Location — Camera animation
Tip: For the smoothest visual effect, use the interpolated animation approach with ValueAnimator. The step-by-step method is simpler but creates visible "jumps" between points. Interpolation creates a fluid drawing motion.