Skip to content

Animated SymbolLayer

Note

You can find the full source code of this example in AnimatedSymbolLayerActivity.kt of the MapLibreAndroidTestApp.

Map data OpenStreetMap. © OpenMapTiles.

Notice that there are (red) cars randomly moving around, and a (yellow) taxi that is always heading to the passenger (indicated by the M symbol), which upon arrival hops to a different location again. We will focus on the passanger and the taxi, because the cars randomly moving around follow a similar pattern.

In a real application you would of course retrieve the locations from some sort of external API, but for the purposes of this example a random latitude longtitude pair within bounds of the currently visible screen will do.

Getter method to get a random location on the screen
private val latLngInBounds: LatLng
    get() {
        val bounds = maplibreMap.projection.visibleRegion.latLngBounds
        val generator = Random()

        val randomLat = bounds.latitudeSouth + generator.nextDouble() * (bounds.latitudeNorth - bounds.latitudeSouth)
        val randomLon = bounds.longitudeWest + generator.nextDouble() * (bounds.longitudeEast - bounds.longitudeWest)

        return LatLng(randomLat, randomLon)
    }
Adding a passenger at a random location (on screen)
private fun addPassenger(style: Style) {
    passenger = latLngInBounds
    val featureCollection = FeatureCollection.fromFeatures(
        arrayOf(
            Feature.fromGeometry(
                Point.fromLngLat(
                    passenger!!.longitude,
                    passenger!!.latitude
                )
            )
        )
    )
    style.addImage(
        PASSENGER,
        ResourcesCompat.getDrawable(resources, R.drawable.icon_burned, theme)!!
    )
    val geoJsonSource = GeoJsonSource(PASSENGER_SOURCE, featureCollection)
    style.addSource(geoJsonSource)
    val symbolLayer = SymbolLayer(PASSENGER_LAYER, PASSENGER_SOURCE)
    symbolLayer.withProperties(
        PropertyFactory.iconImage(PASSENGER),
        PropertyFactory.iconIgnorePlacement(true),
        PropertyFactory.iconAllowOverlap(true)
    )
    style.addLayerBelow(symbolLayer, RANDOM_CAR_LAYER)
}

Adding the taxi on screen is done very similarly.

Adding the taxi with bearing
private fun addTaxi(style: Style) {
    val latLng = latLngInBounds
    val properties = JsonObject()
    properties.addProperty(PROPERTY_BEARING, Car.getBearing(latLng, passenger))
    val feature = Feature.fromGeometry(
        Point.fromLngLat(
            latLng.longitude,
            latLng.latitude
        ),
        properties
    )
    val featureCollection = FeatureCollection.fromFeatures(arrayOf(feature))
    taxi = Car(feature, passenger, duration)
    style.addImage(
        TAXI,
        (ResourcesCompat.getDrawable(resources, R.drawable.ic_taxi_top, theme) as BitmapDrawable).bitmap
    )
    taxiSource = GeoJsonSource(TAXI_SOURCE, featureCollection)
    style.addSource(taxiSource!!)
    val symbolLayer = SymbolLayer(TAXI_LAYER, TAXI_SOURCE)
    symbolLayer.withProperties(
        PropertyFactory.iconImage(TAXI),
        PropertyFactory.iconRotate(Expression.get(PROPERTY_BEARING)),
        PropertyFactory.iconAllowOverlap(true),
        PropertyFactory.iconIgnorePlacement(true)
    )
    style.addLayer(symbolLayer)
}

For animating the taxi we use a ValueAnimator.

Animate the taxi driving towards the passenger
private fun animateTaxi(style: Style) {
    val valueAnimator = ValueAnimator.ofObject(LatLngEvaluator(), taxi!!.current, taxi!!.next)
    valueAnimator.addUpdateListener(object : AnimatorUpdateListener {
        private var latLng: LatLng? = null
        override fun onAnimationUpdate(animation: ValueAnimator) {
            latLng = animation.animatedValue as LatLng
            taxi!!.current = latLng
            updateTaxiSource()
        }
    })
    valueAnimator.addListener(object : AnimatorListenerAdapter() {
        override fun onAnimationEnd(animation: Animator) {
            super.onAnimationEnd(animation)
            updatePassenger(style)
            animateTaxi(style)
        }
    })
    valueAnimator.addListener(object : AnimatorListenerAdapter() {
        override fun onAnimationStart(animation: Animator) {
            super.onAnimationStart(animation)
            taxi!!.feature.properties()!!
                .addProperty("bearing", Car.getBearing(taxi!!.current, taxi!!.next))
        }
    })
    valueAnimator.duration = (7 * taxi!!.current!!.distanceTo(taxi!!.next!!)).toLong()
    valueAnimator.interpolator = AccelerateDecelerateInterpolator()
    valueAnimator.start()
    animators.add(valueAnimator)
}