Skip to content

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

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


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.