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
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
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
- Animation Types — Camera animation comparison
- Camera Position — Camera position fundamentals
- Fly to a Location — Point-to-point flights
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.