Skip to content
This site is a preview of the main branch. The published documentation is at maplibre.org.

Control the camera

The camera decides which part of the map is on screen. A CameraPosition has a center position, a zoom level, a bearing, and a pitch. At zoom 0 the whole world is 512 dp wide, and each zoom level doubles the scale. Bearing is the compass direction at the top of the screen, in degrees clockwise from north. Pitch tilts the map, in degrees away from looking straight down.

Pass initialCameraPosition to rememberMapState:

App.kt
val mapState =
rememberMapState(
initialCameraPosition =
CameraPosition(center = Position(latitude = 45.521, longitude = -122.675), zoom = 13.0)
)
MaplibreMap(state = mapState)

The map starts at this position. Later changes to initialCameraPosition do not move the map; use the functions below instead. The map state saves the camera, so the map comes back to the same place after an Android activity is recreated.

Read the current camera from MapState.cameraPosition. A composable that reads it recomposes when the camera moves.

MapState.animateCamera moves the camera smoothly. It is a suspend function, so call it from a coroutine. List only the properties you want to change in a CameraUpdate; the others keep their current values:

App.kt
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
mapState.animateCamera(CameraUpdate(zoom = mapState.cameraPosition.zoom + 1.0))
}
}
) {
Text("Zoom in")
}

If the map is not on screen yet, the call waits until it is, so you can also start an animation from LaunchedEffect when the screen opens.

The animation parameter chooses how the camera travels.

CameraAnimation.Ease is the default. It moves straight to the target in 300 ms. Zoom changes directly from the current level to the target level. Use it for short moves, such as zooming in place or following a moving point. Over long distances the map moves too fast to follow.

CameraAnimation.Fly zooms out, travels, and zooms back in, so the user can see where the camera is going. Use it to move between places that are far apart, such as two cities:

App.kt
LaunchedEffect(mapState, city) {
mapState.animateCamera(
update = CameraUpdate(center = city, zoom = 12.0),
animation = CameraAnimation.Fly { duration = 3.seconds },
)
}
AB
AB
Start
AB
Travelling
AB
End
Ease

Moves directly to the target.

AB
AB
Start
AB
Travelling
AB
End
Fly

Zooms out, travels, then zooms back in.

Without a duration, the flight takes longer for longer distances. Set speed to change how fast it travels. To change the length of an ease, set its duration in the same way: CameraAnimation.Ease { duration = 1.seconds }.

MapState.setCameraPosition moves the camera at once. It replaces every camera property, so copy the current position and change only what you need:

App.kt
fun showPlace(mapState: MapState, place: Position) {
mapState.setCameraPosition(mapState.cameraPosition.copy(center = place, zoom = 15.0))
}

You can call it before the map is on screen. The map then starts at that position.

MapState.animateCameraAround changes zoom, bearing, or pitch while one point stays at the same place on screen. For example, zoom in on a place the user selected without moving that place on screen:

App.kt
suspend fun zoomInOn(mapState: MapState, place: Position) {
mapState.animateCameraAround(
anchor = CameraAnchor.Geographic(place),
zoom = mapState.cameraPosition.zoom + 2.0,
)
}

CameraAnchor.Geographic keeps a geographic position in place. CameraAnchor.Screen keeps a screen point in place, in dp from the map’s top-left corner. The point must be visible when the animation starts.

AB
AB
Before
AB
After zooming in

The point under the cursor stays in place.

MapState.animateCameraToBounds moves the camera so a BoundingBox fits on screen. Use it to show a whole route or a set of search results. By default it flies.

App.kt
suspend fun showRoute(mapState: MapState, routeBounds: BoundingBox) {
mapState.animateCameraToBounds(
boundingBox = routeBounds,
fit =
CameraFit(fitPadding = DpPadding(left = 32.dp, top = 32.dp, right = 32.dp, bottom = 32.dp)),
)
}

fitPadding in CameraFit keeps space between the box and the edges of the map. The fit also stays clear of the viewportInsets that you pass to MaplibreMap, such as the area under a bottom sheet; see Keep controls clear of your UI. To fit without animation, call MapState.fitCameraToBounds.

fitPaddingBottom sheetviewportInsets
The route bounds fit inside the padding, above the bottom sheet.

MapState.cameraForBounds calculates the fitted camera without moving the map, so you can change it before you apply it. For example, cap the zoom so that a fit around a few nearby points does not zoom in too far:

App.kt
suspend fun showResults(mapState: MapState, resultBounds: BoundingBox) {
val camera = mapState.cameraForBounds(resultBounds)
mapState.animateCamera(
update = camera.copy(zoom = minOf(camera.zoom, 15.0)).toCameraUpdate(),
animation = CameraAnimation.Fly.Standard,
)
}

To fit a list of points or a line instead of a box, use MapState.cameraForCoordinates or MapState.cameraForGeometry.

MapState.viewport describes what the map shows on screen: the camera, the map’s size, and the geographic area in view. Use it to load data for the visible area or to show the current zoom:

App.kt
val viewport = mapState.viewport
if (viewport != null) {
Text("Zoom: ${viewport.cameraPosition.zoom}")
Text("Visible bounds: ${viewport.visibleBounds}")
}

The viewport is null until the map draws for the first time. A composable that reads it recomposes when the camera moves or the map changes size.

The map repeats the world to the left and right, so longitudes in Viewport.visibleBounds can go past ±180°. Call VisibleBounds.toBoundingBox to get a standard GeoJSON bounding box.

Convert between screen and map coordinates

Section titled “Convert between screen and map coordinates”

MapState.screenLocationFromPosition finds where a geographic position appears on screen. MapState.positionFromScreenLocation finds the geographic position under a screen point. Screen points are in dp from the map’s top-left corner:

App.kt
val centerOnScreen: DpOffset? =
mapState.screenLocationFromPosition(mapState.cameraPosition.center)
val positionAtPoint: Position? =
mapState.positionFromScreenLocation(DpOffset(x = 100.dp, y = 150.dp))

Both return null until the map draws for the first time. To place Compose UI at a geographic position, use MapOverlayScope.placedAt from Overlay Compose UI instead.

Pass CameraConstraints to MaplibreMap to keep the camera inside limits. Use it to keep users in the area your app covers, or to stop them zooming past the detail your map data has:

App.kt
MaplibreMap(
state = mapState,
cameraConstraints =
CameraConstraints {
minZoom = 10.0
maxZoom = 18.0
maxPitch = 0.0
boundingBox = BoundingBox(west = -122.84, south = 45.43, east = -122.47, north = 45.65)
},
)

Properties you leave out keep their defaults: zoom 0 to 20 and pitch 0 to 60. Setting maxPitch to 0 keeps the map flat. The limits apply to gestures and to camera movements from your code. boundingBox stops the user from panning away from that area.