Control the camera
The camera defines the visible part of the map: a target position, a zoom
level, a bearing, and a tilt. rememberMapState creates a
MapState with a read-only camera position. Pass an
initial CameraPosition to set the camera at startup:
val mapState = rememberMapState( initialCameraPosition = CameraPosition(target = Position(latitude = 45.521, longitude = -122.675), zoom = 13.0) )MaplibreMap(state = mapState)Animate the camera
Section titled “Animate the camera”MapState.animateCamera is a suspend function that moves
only the properties specified in a CameraUpdate. Call it from a
coroutine. The function waits for an attached viewport and returns when its
properties finish or are superseded:
LaunchedEffect(mapState) { mapState.animateCamera( update = CameraUpdate(target = Position(latitude = 47.607, longitude = -122.342)) )}Use MapState.setCameraPosition to move without an animation. The
position is retained when the map is detached and restored when it is displayed
again.
Choose the transition
Section titled “Choose the transition”The animation parameter selects one of two transitions. Each platform
implements both with the same controls.
CameraAnimation.Fly follows a flight path. The camera zooms out, crosses
the ground, and zooms back in, so the map stays legible over any distance.
Without a duration, the flight takes as long as its path length and speed
require. Set duration for a fixed time, speed in screenfuls per second for a
constant pace, and minZoom to keep the flight path from zooming out past a
zoom. The path peaks near minZoom rather than stopping exactly at it:
LaunchedEffect(mapState) { mapState.animateCamera( update = CameraUpdate(target = Position(latitude = 40.713, longitude = -74.006), zoom = 12.0), animation = CameraAnimation.Fly(duration = 3.seconds, minZoom = 4.0), )}CameraAnimation.Ease is the default. It moves the camera directly to its target over a
fixed duration, without zooming out on the way. Use it for short moves, such as
changing zoom in place or following a location:
LaunchedEffect(mapState) { mapState.animateCamera( update = CameraUpdate(zoom = mapState.cameraPosition.zoom + 1.0), animation = CameraAnimation.Ease(duration = 500.milliseconds), )}Both transitions accept an easing timing curve, a CubicBezier.
On Android, the system animator duration scale multiplies the duration of
either transition. A scale of zero jumps to the target.
Keep a point fixed on screen
Section titled “Keep a point fixed on screen”MapState.animateCameraAround changes zoom, bearing, or tilt while
keeping a point at its screen location. Use CameraAnchor.Screen for
coordinates in dp from the full map’s top-left corner:
LaunchedEffect(mapState) { mapState.animateCameraAround( anchor = CameraAnchor.Screen(DpOffset(120.dp, 200.dp)), zoom = 16.0, bearing = 90.0, animation = CameraAnimation.Ease(500.milliseconds), )}Use CameraAnchor.Geographic to keep a selected geographic location
at its current screen point. The anchor is resolved when the animation starts
and must be visible on the map. This operation supports easing; it does not
accept a flight or a target center because the center moves to keep the anchor
fixed.
Persistent camera padding participates in projection. Changing viewport insets, resizing the logical viewport, or losing the attachment cancels the animation. Camera constraints take precedence over anchor preservation. The preservation guarantee applies to flat Mercator maps, including tilted cameras; it does not extend to globe or terrain.
Fit a bounding box
Section titled “Fit a bounding box”MapState.animateCameraToBounds fits a BoundingBox in the current
viewport with the same animation choices. Fit padding adds space around the box
inside the viewport insets and camera padding:
LaunchedEffect(mapState) { mapState.animateCameraToBounds( boundingBox = BoundingBox(west = -123.0, south = 47.0, east = -122.0, north = 48.0), fitPadding = DpPadding(left = 32.dp, top = 32.dp, right = 32.dp, bottom = 32.dp), )}Use MapState.fitCameraToBounds to fit the same bounding box without
an animation.
Use MapState.cameraForBounds to calculate a position before applying
it. The query waits for a viewport and leaves the current camera and animation
unchanged. For example, cap the calculated zoom before animating:
LaunchedEffect(mapState) { val camera = mapState.cameraForBounds( boundingBox = BoundingBox(west = -123.0, south = 47.0, east = -122.0, north = 48.0), fitPadding = DpPadding(left = 32.dp, top = 32.dp, right = 32.dp, bottom = 32.dp), ) mapState.animateCamera(camera.copy(zoom = minOf(camera.zoom, 12.0)).toCameraUpdate())}Read the visible area
Section titled “Read the visible area”MapState.viewport reports the last rendered camera, size,
visible bounds, and visible region, which all describe the same frame. It is
null until the map renders its first viewport, while
MapState.cameraPosition is available before that. A composition
that reads the viewport recomposes when the camera moves or the map resizes:
val viewport = mapState.viewportif (viewport != null) { Text("Visible bounds: ${viewport.visibleBounds}")}Convert between screen and geographic coordinates
Section titled “Convert between screen and geographic coordinates”MapState.screenLocationFromPosition converts a geographic
position to an offset from the top-left corner of the map composable.
MapState.positionFromScreenLocation converts in the other
direction:
val screenOffset = mapState.screenLocationFromPosition(mapState.cameraPosition.target)val geoPosition = mapState.positionFromScreenLocation(DpOffset(x = 100.dp, y = 150.dp))The repeated world and the antimeridian
Section titled “The repeated world and the antimeridian”MapLibre repeats the world horizontally. Geographic values read from the map,
such as Viewport.visibleBounds and
MapState.positionFromScreenLocation, preserve the world copy:
longitudes may extend past ±180°, and the visible bounds may span more than
360°. VisibleBounds.toBoundingBox() converts to a GeoJSON BoundingBox, where an
antimeridian crossing follows RFC 7946 with an east longitude less than the
west.