Skip to content

Present on an Android Surface

AndroidMapPresentation presents a MapState on an Android Surface without a Compose UI view. The same state can move between a Surface and MaplibreMap. The Android Auto demo uses it in a car app.

Create the state with MapRuntime.createMapState. Its base style, style content, camera operations, and click handlers work as they do with MaplibreMap. One presentation presents a state at a time. Close it before presenting the state elsewhere.

Construct the presentation on the main thread with a Lifecycle. Rendering pauses while the lifecycle is stopped.

SurfaceMapHost.kt
/** Your code owns the MapState and each Surface. Call these methods on the main thread. */
class SurfaceMapHost(context: Context, state: MapState, lifecycle: Lifecycle) : AutoCloseable {
val presentation = AndroidMapPresentation(context, state, lifecycle)
private var binding: AndroidMapPresentation.SurfaceBinding? = null
fun onSurfaceAvailable(surface: Surface, width: Int, height: Int, density: Float) {
binding = presentation.attachSurface(surface, width, height, density)
}
fun onSurfaceChanged(width: Int, height: Int, density: Float) {
binding?.update(width, height, density)
}
fun onSurfaceDestroyed() {
binding?.close()
binding = null
}
fun onConfigurationChanged(configuration: Configuration) {
presentation.updateConfiguration(configuration)
}
override fun close() {
presentation.close()
}
}

attachSurface takes the Surface’s size in physical pixels and its density in physical pixels per logical pixel. Keep the returned AndroidMapPresentation.SurfaceBinding to update the size or density, and close it before releasing the Surface.

Forward configuration changes with updateConfiguration so the map follows the host’s locale, font scale, layout direction, and theme. See AndroidMapPresentation for presentation lifecycle and failure handling.

The presentation has no gesture recognizer. Pass gestures your code recognized to MapState.panBy, MapState.fling, MapState.scaleBy, and MapState.click. They follow the camera permissions and callbacks in the presentation’s interactions. Convert physical pixels to logical pixels first.

SurfaceInput.kt
/** Converts physical pixels to the map's logical pixels before passing gestures. */
class SurfaceInput(private val state: MapState, private val density: Float) {
fun pan(deltaX: Float, deltaY: Float) {
state.panBy(logical(deltaX, deltaY))
}
fun fling(velocityX: Float, velocityY: Float) {
state.fling(logical(velocityX, velocityY))
}
fun scale(factor: Float, focusX: Float?, focusY: Float?) {
val anchor = if (focusX != null && focusY != null) logical(focusX, focusY) else null
state.scaleBy(factor.toDouble(), anchor)
}
fun click(x: Float, y: Float) {
state.click(logical(x, y))
}
private fun logical(x: Float, y: Float) = DpOffset((x / density).dp, (y / density).dp)
}