Skip to content

Animate Camera Orbiting a Point

This tutorial shows how to create a smooth orbiting camera animation that rotates around a central point — great for showcasing landmarks or creating cinematic map experiences.

Prerequisites

Basic Orbit Animation

Rotate the camera 360° around a point:

kotlin
import android.animation.ValueAnimator
import android.os.Bundle
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.maps.MapView
import org.maplibre.android.maps.MapMetricsMap
import org.maplibre.android.maps.Style

class OrbitActivity : AppCompatActivity() {

    private lateinit var mapView: MapView
    private lateinit var map: MapMetricsMap
    private var orbitAnimator: ValueAnimator? = null

    private val center = LatLng(48.8584, 2.2945) // Eiffel Tower

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_orbit)

        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"
                )
            )

            // Set initial camera with tilt
            map.cameraPosition = CameraPosition.Builder()
                .target(center)
                .zoom(16.0)
                .tilt(55.0)
                .bearing(0.0)
                .build()

            // Start orbit button
            findViewById<Button>(R.id.btnOrbit).setOnClickListener {
                startOrbit()
            }

            // Stop button
            findViewById<Button>(R.id.btnStop).setOnClickListener {
                stopOrbit()
            }
        }
    }

    private fun startOrbit() {
        orbitAnimator?.cancel()

        val startBearing = map.cameraPosition.bearing

        orbitAnimator = ValueAnimator.ofFloat(0f, 360f).apply {
            duration = 20000 // 20 seconds for full rotation
            interpolator = LinearInterpolator()
            repeatCount = ValueAnimator.INFINITE

            addUpdateListener { animation ->
                val bearing = startBearing + (animation.animatedValue as Float)

                map.moveCamera(
                    CameraUpdateFactory.newCameraPosition(
                        CameraPosition.Builder()
                            .target(center)
                            .zoom(16.0)
                            .tilt(55.0)
                            .bearing(bearing.toDouble() % 360)
                            .build()
                    )
                )
            }

            start()
        }
    }

    private fun stopOrbit() {
        orbitAnimator?.cancel()
        orbitAnimator = null
    }

    override fun onDestroy() {
        stopOrbit()
        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)
    }
}

Orbit with Zoom Breathing

Add subtle zoom pulsing while orbiting:

kotlin
import android.animation.AnimatorSet

private fun startBreathingOrbit() {
    // Bearing rotation
    val bearingAnimator = ValueAnimator.ofFloat(0f, 360f).apply {
        duration = 20000
        interpolator = LinearInterpolator()
        repeatCount = ValueAnimator.INFINITE
    }

    // Zoom breathing (zoom in and out gently)
    val zoomAnimator = ValueAnimator.ofFloat(15.5f, 16.5f).apply {
        duration = 5000
        repeatMode = ValueAnimator.REVERSE
        repeatCount = ValueAnimator.INFINITE
    }

    // Combined update
    var currentBearing = 0f
    var currentZoom = 16f

    bearingAnimator.addUpdateListener { currentBearing = it.animatedValue as Float }
    zoomAnimator.addUpdateListener { currentZoom = it.animatedValue as Float }

    // Frame update on bearing changes
    bearingAnimator.addUpdateListener {
        map.moveCamera(
            CameraUpdateFactory.newCameraPosition(
                CameraPosition.Builder()
                    .target(center)
                    .zoom(currentZoom.toDouble())
                    .tilt(55.0)
                    .bearing(currentBearing.toDouble() % 360)
                    .build()
            )
        )
    }

    val animatorSet = AnimatorSet()
    animatorSet.playTogether(bearingAnimator, zoomAnimator)
    animatorSet.start()
}

Orbit Then Fly Away

Orbit for one revolution then fly to a new location:

kotlin
private fun orbitThenFlyAway() {
    val startBearing = map.cameraPosition.bearing

    ValueAnimator.ofFloat(0f, 360f).apply {
        duration = 10000
        interpolator = LinearInterpolator()

        addUpdateListener { animation ->
            val bearing = startBearing + (animation.animatedValue as Float)
            map.moveCamera(
                CameraUpdateFactory.newCameraPosition(
                    CameraPosition.Builder()
                        .target(center)
                        .zoom(16.0)
                        .tilt(55.0)
                        .bearing(bearing.toDouble() % 360)
                        .build()
                )
            )
        }

        addListener(object : android.animation.AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: android.animation.Animator) {
                // Fly to next location after orbit completes
                map.animateCamera(
                    CameraUpdateFactory.newCameraPosition(
                        CameraPosition.Builder()
                            .target(LatLng(48.8738, 2.2950)) // Arc de Triomphe
                            .zoom(16.0)
                            .tilt(55.0)
                            .bearing(0.0)
                            .build()
                    ),
                    3000
                )
            }
        })

        start()
    }
}

Touch-to-Stop Orbit

Stop orbiting when the user touches the map:

kotlin
private fun startOrbitWithTouchStop() {
    startOrbit()

    // Stop orbit on any touch
    map.addOnCameraMoveStartedListener { reason ->
        if (reason == MapMetricsMap.OnCameraMoveStartedListener.REASON_API_GESTURE) {
            stopOrbit()
        }
    }
}

Next Steps


Tip: Use moveCamera (not animateCamera) inside the orbit loop — animateCamera adds its own easing which conflicts with the ValueAnimator. The ValueAnimator handles all timing; the camera just needs instant updates each frame.