Skip to content

Slowly Fly to a Location

This tutorial shows how to create a cinematic slow camera flight — useful for showcases, presentations, or atmospheric map experiences.

Prerequisites

Cinematic Slow Flight

A long-duration animated camera move with tilt and bearing changes:

kotlin
import android.os.Bundle
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 SlowFlyActivity : AppCompatActivity() {

    private lateinit var mapView: MapView
    private lateinit var map: MapMetricsMap

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

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

            // Start from a wide aerial view
            map.cameraPosition = CameraPosition.Builder()
                .target(LatLng(48.8566, 2.3522)) // Paris overview
                .zoom(10.0)
                .tilt(0.0)
                .bearing(0.0)
                .build()

            // Slow fly button
            findViewById<Button>(R.id.btnSlowFly).setOnClickListener {
                slowFlyToEiffelTower()
            }
        }
    }

    private fun slowFlyToEiffelTower() {
        val destination = CameraPosition.Builder()
            .target(LatLng(48.8584, 2.2945)) // Eiffel Tower
            .zoom(17.0)                       // Street level
            .tilt(60.0)                       // Dramatic 3D angle
            .bearing(-30.0)                   // Slight rotation
            .build()

        map.animateCamera(
            CameraUpdateFactory.newCameraPosition(destination),
            10000 // 10 seconds — slow, cinematic
        )
    }

    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 onDestroy() { super.onDestroy(); mapView.onDestroy() }
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        mapView.onSaveInstanceState(outState)
    }
}

Multi-Stage Cinematic Sequence

Chain multiple slow flights for a full cinematic experience:

kotlin
private data class CinematicStop(
    val target: LatLng,
    val zoom: Double,
    val tilt: Double,
    val bearing: Double,
    val flightDuration: Long,   // ms to fly there
    val pauseDuration: Long     // ms to pause before next
)

private val cinematicStops = listOf(
    CinematicStop(
        LatLng(48.8566, 2.3522), 11.0, 0.0, 0.0,
        0, 1000   // Start: Paris overview
    ),
    CinematicStop(
        LatLng(48.8584, 2.2945), 16.0, 55.0, -20.0,
        8000, 3000  // Fly to Eiffel Tower
    ),
    CinematicStop(
        LatLng(48.8606, 2.3376), 16.5, 50.0, 30.0,
        6000, 3000  // Fly to Louvre
    ),
    CinematicStop(
        LatLng(48.8530, 2.3499), 17.0, 60.0, -10.0,
        6000, 3000  // Fly to Notre-Dame
    ),
    CinematicStop(
        LatLng(48.8867, 2.3431), 15.5, 45.0, 0.0,
        8000, 2000  // Fly to Sacré-Cœur
    ),
)

private fun startCinematicTour() {
    playCinematicStop(0)
}

private fun playCinematicStop(index: Int) {
    if (index >= cinematicStops.size) return

    val stop = cinematicStops[index]
    val position = CameraPosition.Builder()
        .target(stop.target)
        .zoom(stop.zoom)
        .tilt(stop.tilt)
        .bearing(stop.bearing)
        .build()

    if (stop.flightDuration == 0L) {
        // First stop — instant position
        map.moveCamera(CameraUpdateFactory.newCameraPosition(position))
        android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
            playCinematicStop(index + 1)
        }, stop.pauseDuration)
    } else {
        map.animateCamera(
            CameraUpdateFactory.newCameraPosition(position),
            stop.flightDuration.toInt(),
            object : MapMetricsMap.CancelableCallback {
                override fun onFinish() {
                    android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
                        playCinematicStop(index + 1)
                    }, stop.pauseDuration)
                }

                override fun onCancel() {
                    // User interrupted — stop the tour
                }
            }
        )
    }
}

Slow Fly with easeCamera

Use easeCamera for a constant-speed flight (no acceleration):

kotlin
private fun slowEaseFlight() {
    map.easeCamera(
        CameraUpdateFactory.newCameraPosition(
            CameraPosition.Builder()
                .target(LatLng(48.8584, 2.2945))
                .zoom(16.0)
                .tilt(50.0)
                .bearing(-15.0)
                .build()
        ),
        15000 // 15 seconds, constant speed
    )
}

Speed Comparison

DurationFeelUse Case
1-2sQuick snapUI navigation
3-5sNormal fly-toStandard interaction
6-10sSlow, cinematicShowcase, guided tour
10-20sVery slowPresentation, ambient display
easeCamera 10s+Constant speed, no accelerationSmooth documentary feel

Next Steps


Tip: For the most cinematic effect, combine slow flights (8-15s) with tilt changes (0° → 60°) and slight bearing shifts. The simultaneous zoom + tilt + bearing creates the "Google Earth" swooping effect.