Skip to content
This site is a preview of pull request #1363.

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. Destroying the lifecycle closes the presentation. Closing the state also closes it.

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 and pass size or density changes to update. A density change recreates the native map.

Close the binding before releasing the Surface. Closing waits for rendering to stop using the Surface. Attaching another Surface closes the previous binding. A closed binding ignores later updates.

The map produces no frames until its style can be presented. Show your own loading content on the Surface until then. A map without a style renders black.

Pass a Configuration to updateConfiguration when the locale, layout direction, font scale, or night mode changes. Style content reads it through LocalConfiguration and LocalContext, and reads the Surface density through LocalDensity. UI composables and view-dependent locals are not available in style content.

failure reports an error that ended the presentation. Create a new presentation to retry.

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, interrupt camera animations, and dispatch clicks to interactive layers. 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)
}