Set Pitch and Bearing
This tutorial shows how to control the 3D perspective (pitch/tilt) and compass direction (bearing/rotation) of your MapMetrics Android map.
Prerequisites
- Completed the Getting Started Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Set Pitch and Bearing on Load
Configure the initial 3D view:
kotlin
import android.os.Bundle
import android.widget.Button
import android.widget.SeekBar
import android.widget.TextView
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 PitchBearingActivity : AppCompatActivity() {
private lateinit var mapView: MapView
private lateinit var map: MapMetricsMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_pitch_bearing)
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 3D perspective
map.cameraPosition = CameraPosition.Builder()
.target(LatLng(48.8584, 2.2945))
.zoom(16.0)
.tilt(60.0) // Pitch: 0 = top-down, 60 = dramatic 3D
.bearing(45.0) // Bearing: 0 = north up, 45 = northeast
.build()
setupControls()
}
}
private fun setupControls() {
val pitchText = findViewById<TextView>(R.id.tvPitch)
val bearingText = findViewById<TextView>(R.id.tvBearing)
// Pitch slider (0-60)
findViewById<SeekBar>(R.id.seekPitch).apply {
max = 60
progress = map.cameraPosition.tilt.toInt()
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, value: Int, user: Boolean) {
if (!user) return
pitchText.text = "Pitch: $value°"
map.moveCamera(
CameraUpdateFactory.newCameraPosition(
CameraPosition.Builder(map.cameraPosition)
.tilt(value.toDouble())
.build()
)
)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
// Bearing slider (0-360)
findViewById<SeekBar>(R.id.seekBearing).apply {
max = 360
progress = map.cameraPosition.bearing.toInt()
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, value: Int, user: Boolean) {
if (!user) return
bearingText.text = "Bearing: $value°"
map.moveCamera(
CameraUpdateFactory.newCameraPosition(
CameraPosition.Builder(map.cameraPosition)
.bearing(value.toDouble())
.build()
)
)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
}
}
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)
}
}Preset Camera Views
Offer buttons for common perspective angles:
kotlin
private fun setupPresets() {
// Top-down (2D)
findViewById<Button>(R.id.btnTopDown).setOnClickListener {
animateTo(tilt = 0.0, bearing = 0.0, zoom = 14.0)
}
// Gentle 3D
findViewById<Button>(R.id.btnGentle3d).setOnClickListener {
animateTo(tilt = 30.0, bearing = 0.0, zoom = 15.0)
}
// Dramatic 3D
findViewById<Button>(R.id.btnDramatic3d).setOnClickListener {
animateTo(tilt = 60.0, bearing = -30.0, zoom = 16.0)
}
// Street level
findViewById<Button>(R.id.btnStreetLevel).setOnClickListener {
animateTo(tilt = 60.0, bearing = 90.0, zoom = 18.0)
}
// Reset to north
findViewById<Button>(R.id.btnResetNorth).setOnClickListener {
animateTo(tilt = 0.0, bearing = 0.0, zoom = map.cameraPosition.zoom)
}
}
private fun animateTo(tilt: Double, bearing: Double, zoom: Double) {
map.animateCamera(
CameraUpdateFactory.newCameraPosition(
CameraPosition.Builder()
.target(map.cameraPosition.target)
.zoom(zoom)
.tilt(tilt)
.bearing(bearing)
.build()
),
1500
)
}Limit Pitch Range via XML
Set pitch limits in the layout:
xml
<org.maplibre.android.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:maplibre_cameraPitchMax="60"
app:maplibre_cameraPitchMin="0"
app:maplibre_cameraTilt="45"
app:maplibre_cameraBearing="0"
app:maplibre_cameraZoom="15" />Limit Pitch Range Programmatically
kotlin
val options = MapMetricsMapOptions.createFromAttributes(this, null)
.maxPitchPreference(60.0)
.minPitchPreference(0.0)
.camera(
CameraPosition.Builder()
.target(LatLng(48.8584, 2.2945))
.zoom(15.0)
.tilt(45.0)
.build()
)
mapView = MapView(this, options)Pitch and Bearing Reference
| Property | Range | Description |
|---|---|---|
| Tilt/Pitch | 0 - 60 | 0 = top-down, 60 = maximum 3D perspective |
| Bearing | 0 - 360 | 0/360 = north up, 90 = east up, 180 = south up, 270 = west up |
Common Perspective Combinations
| View | Tilt | Bearing | Zoom | Effect |
|---|---|---|---|---|
| Standard 2D | 0° | 0° | 12-14 | Classic flat map |
| Gentle 3D | 30° | 0° | 14-16 | Subtle depth |
| Dramatic 3D | 60° | Any | 15-17 | Skyline visible |
| Street Level | 60° | Route direction | 17-19 | Navigation feel |
| Cinematic | 55° | Slowly rotating | 16 | Showcase mode |
Next Steps
- Camera Position — Camera position fundamentals
- Orbit Animation — Rotating camera animation
- Building Layer — 3D buildings look best with tilt
Tip: 3D buildings and fill-extrusion layers are most impressive at tilt 45-60°. At tilt 0° (top-down), 3D extrusions are invisible since you're looking straight down on them.