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

Handle map input and clicks

Users can pan, zoom, rotate, and tilt the map by default. Pass MapInteractions to customize controls or respond to input. The configuration has three parts:

  • camera: which movements the controls allow, such as panning or rotation.
  • bindings: what gestures, scrolling, and keys do.
  • callbacks: how your app responds to clicks.

For a map that users can pan and zoom but cannot rotate or tilt, disable those camera movements:

App.kt
MaplibreMap(
interactions =
MapInteractions {
camera {
rotate { enabled = false }
tilt { enabled = false }
}
}
)

Omitted settings keep their defaults. Use MapInteractions(from = existing) to build on another configuration, or MapInteractions.None to disable built-in input, including feature clicks.

To stop following the user’s location when they pan, use camera.pan.onStart. This hook runs before the component responds to input and can run again within the same gesture. It does not report when camera movement ends. For moving the camera from app code, see Control the camera.

Vertical scrolling zooms by default. This example makes scrolling pan the map, while Ctrl-scroll zooms:

App.kt
MaplibreMap(
interactions =
MapInteractions {
bindings {
scroll {
mappings {
on(modifiers = Containing(KeyModifier.Ctrl), response = ScrollResponse.Zoom)
otherwise(ScrollResponse.Pan)
}
}
}
}
)

A mappings block replaces the default responses for that input. Rows are tried from top to bottom; otherwise handles the remaining input. Available gestures depend on the input device and Compose host.

A null pointer-type, button, or modifier filter matches any value. Key mappings default to exactly no modifiers; set modifiers = null to match a key with any modifiers.

Use callbacks.click to act on a geographic location. For example, pass the clicked position to your app:

App.kt
@Composable
fun ClickableMap(onLocationSelected: (Position) -> Unit) {
MaplibreMap(
interactions =
MapInteractions {
callbacks {
click {
onEvent { event ->
event.position?.let(onLocationSelected)
ClickResult.Consume
}
}
}
}
)
}

Return ClickResult.Consume when your app handles the click, or ClickResult.Pass to let it continue to interactive layers. Use longClick for a long press or secondary mouse click.

A layer’s onClick receives the features under the pointer. Use hitPadding to make small features easier to select:

App.kt
val state = rememberMapState {
val interactiveAmtrakStations =
rememberGeoJsonSource(GeoJsonData.Uri(Res.getUri("files/data/amtrak_stations.geojson")))
CircleLayer(
id = "amtrak-stations",
source = interactiveAmtrakStations,
hitPadding = 12.dp,
onClick = { features ->
println("Clicked on ${features[0].toJson()}")
ClickResult.Consume
},
)
}
MaplibreMap(state = state)

Layers receive clicks from front to back until a handler returns ClickResult.Consume. Use callbacks.click.onUnhandled for clicks that no layer handles, such as clearing an app’s selection.

Use camera.rotate.snapping to snap to nearby bearings after rotation ends. Choose specific bearings or evenly spaced targets, such as the four cardinal directions:

MaplibreMap(
interactions =
MapInteractions {
camera {
rotate {
snapping {
targets = BearingTargets.evenlySpaced(count = 4)
tolerance = 7.0
}
}
}
}
)

Use camera.rotate.haptics to add feedback at chosen bearings while rotating. Haptics work independently of snapping and support Android, iOS, and macOS. Combine notches with different emphasis levels:

MaplibreMap(
interactions =
MapInteractions {
camera {
rotate {
haptics {
notch(BearingTargets.evenlySpaced(24), HapticEmphasis.Subtle)
notch(BearingTargets.evenlySpaced(4), HapticEmphasis.Standard)
notch(BearingTargets.at(0.0), HapticEmphasis.Emphasized)
}
}
}
}
)