Compose provides a variety of APIs to help you detect gestures that are generated from user interactions. The APIs cover a wide range of use cases:
Some of them are high-level and designed to cover the most commonly used gestures. For example, the
clickable
modifier allows easy detection of a click, and it also provides accessibility features and displays visual indicators when tapped (such as ripples).There are also less commonly used gesture detectors that offer more flexibility on a lower level, like
PointerInputScope.detectTapGestures
orPointerInputScope.detectDragGestures
but don't include the extra features.
Tapping and pressing
The
clickable
modifier allows apps to detect clicks on the element it's applied to.
@Composable
fun ClickableSample() {
val count = remember { mutableStateOf(0) }
// content that you want to make clickable
Text(
text = count.value.toString(),
modifier = Modifier.clickable { count.value += 1 }
)
}
When more flexibility is needed, you can provide a tap gesture detector via the
pointerInput
modifier:
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = { /* Called when the gesture starts */ },
onDoubleTap = { /* Called on Double Tap */ },
onLongPress = { /* Called on Long Press */ },
onTap = { /* Called on Tap */ }
)
}
Scrolling
Scroll modifiers
The
verticalScroll
and
horizontalScroll
modifiers provide the simplest way to allow the user to scroll an element when
the bounds of its contents are larger than its maximum size constraints. With
the verticalScroll
and horizontalScroll
modifiers you don't need to
translate or offset the contents.
@Composable
fun ScrollBoxes() {
Column(
modifier = Modifier
.background(Color.LightGray)
.size(100.dp)
.verticalScroll(rememberScrollState())
) {
repeat(10) {
Text("Item $it", modifier = Modifier.padding(2.dp))
}
}
}
The
ScrollState
allows you to change the scroll position or get its current state. To create it
with default parameters, use
rememberScrollState()
.
@Composable
private fun ScrollBoxesSmooth() {
// Smoothly scroll 100px on first composition
val state = rememberScrollState()
LaunchedEffect(Unit) { state.animateScrollTo(100) }
Column(
modifier = Modifier
.background(Color.LightGray)
.size(100.dp)
.padding(horizontal = 8.dp)
.verticalScroll(state)
) {
repeat(10) {
Text("Item $it", modifier = Modifier.padding(2.dp))
}
}
}
Scrollable modifier
The
scrollable
modifier differs from the scroll modifiers in that scrollable
detects the
scroll gestures, but does not offset its contents. A
ScrollableState
is required for this modifier to work correctly.
When constructing ScrollableState
you must provide a
consumeScrollDelta
function which will be invoked on each scroll step
(by gesture input, smooth scrolling or flinging) with the delta in pixels.
This function must return the amount of scrolling distance consumed, to
ensure the event is properly propagated in cases where there are nested
elements that have the scrollable
modifier.
The following snippet detects the gestures and displays a numerical value for an offset, but does not offset any elements:
@Composable
fun ScrollableSample() {
// actual composable state
var offset by remember { mutableStateOf(0f) }
Box(
Modifier
.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
// Scrollable state: describes how to consume
// scrolling delta and update offset
state = rememberScrollableState { delta ->
offset += delta
delta
}
)
.background(Color.LightGray),
contentAlignment = Alignment.Center
) {
Text(offset.toString())
}
}
Nested Scrolling
Compose supports nested scrolling, in which multiple elements react to a single scroll gesture. A typical example of nested scrolling is a list inside another list, and a more complex case is a collapsing toolbar.
Automatic nested scrolling
Simple nested scrolling requires no action on your part. Gestures that initiate a scrolling action are propagated from children to parents automatically, such that when the child can't scroll any further, the gesture is handled by its parent element.
Automatic nested scrolling is supported and provided out of the box by some of
Compose's components and modifiers: verticalScroll
, horizontalScroll
,
scrollable
, Lazy
APIs and TextField
. This means that when the user
scrolls an inner child of nested components, the previous modifiers propagate
the scrolling deltas to the parents that have nested scrolling support.
The following example shows elements with a verticalScroll
modifier applied to it inside a container that also has a verticalScroll
modifier applied to it.
val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
Box(
modifier = Modifier
.background(Color.LightGray)
.verticalScroll(rememberScrollState())
.padding(32.dp)
) {
Column {
repeat(6) {
Box(
modifier = Modifier
.height(128.dp)
.verticalScroll(rememberScrollState())
) {
Text(
"Scroll here",
modifier = Modifier
.border(12.dp, Color.DarkGray)
.background(brush = gradient)
.padding(24.dp)
.height(150.dp)
)
}
}
}
}
Using the nestedScroll modifier
If you need to create an advanced coordinated scroll between multiple elements,
the
nestedScroll
modifier gives you more flexibility by defining a nested scrolling hierarchy.
Note that some components have built-in nested scroll support.
You can use nestedScroll
to confer such support to other components,
including custom components.
Dragging
The
draggable
modifier is the high-level entry point to drag gestures in a single orientation,
and reports the drag distance in pixels.
It's important to note that this modifier is similar to scrollable
, in that it
only detects the gesture. You need to hold the state and represent it on screen
by, for example, moving the element via the
offset
modifier:
var offsetX by remember { mutableStateOf(0f) }
Text(
modifier = Modifier
.offset { IntOffset(offsetX.roundToInt(), 0) }
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offsetX += delta
}
),
text = "Drag me!"
)
If you need to control the whole dragging gesture, consider using the drag
gesture detector instead, via the
pointerInput
modifier.
Box(modifier = Modifier.fillMaxSize()) {
var offsetX by remember { mutableStateOf(0f) }
var offsetY by remember { mutableStateOf(0f) }
Box(
Modifier
.offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
.background(Color.Blue)
.size(50.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consumeAllChanges()
offsetX += dragAmount.x
offsetY += dragAmount.y
}
}
)
}
Swiping
The
swipeable
modifier lets you drag elements which, when released, animate towards typically
two or more anchor points defined in an orientation. A common usage for this is
to implement a ‘swipe-to-dismiss’ pattern.
It's important to note that this modifier does not move the element, it only
detects the gesture. You need to hold the state and represent it on screen by,
for example, moving the element via the
offset
modifier.
The swipeable state is required in the swipeable
modifier, and can be created
and remembered with
rememberSwipeableState()
.
This state also provides a set of useful methods to programmatically animate to
anchors (see
snapTo
,
animateTo
,
performFling
,
and
performDrag
)
as well as properties to observe the dragging progress.
The swipe gesture can be configured to have different threshold types, such as
FixedThreshold(Dp)
and
FractionalThreshold(Float)
,
and they can be different for each anchor point from-to combination.
For more flexibility, you can configure the resistance
when swiping past the
bounds and, also, the velocityThreshold
which will animate a swipe to the
next state, even if the positional thresholds
have not been reached.
@Composable
fun SwipeableSample() {
val width = 96.dp
val squareSize = 48.dp
val swipeableState = rememberSwipeableState(0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
val anchors = mapOf(0f to 0, sizePx to 1) // Maps anchor points (in px) to states
Box(
modifier = Modifier
.width(width)
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.3f) },
orientation = Orientation.Horizontal
)
.background(Color.LightGray)
) {
Box(
Modifier
.offset { IntOffset(swipeableState.offset.value.roundToInt(), 0) }
.size(squareSize)
.background(Color.DarkGray)
)
}
}
Multitouch: Panning, zooming, rotating
To detect multitouch gestures used for panning, zooming and rotating, you can
use the transformable
modifier. This modifier does not transform elements by
itself, it only detects the gestures.
@Composable
fun TransformableSample() {
// set up all transformation states
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
var offset by remember { mutableStateOf(Offset.Zero) }
val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
scale *= zoomChange
rotation += rotationChange
offset += offsetChange
}
Box(
Modifier
// apply other transformations like rotation and zoom
// on the pizza slice emoji
.graphicsLayer(
scaleX = scale,
scaleY = scale,
rotationZ = rotation,
translationX = offset.x,
translationY = offset.y
)
// add transformable to listen to multitouch transformation events
// after offset
.transformable(state = state)
.background(Color.Blue)
.fillMaxSize()
)
}
If you need to combine zooming, panning and rotation with other gestures, you
can use the
PointerInputScope.detectTransformGestures
detector.