Gestures

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 or PointerInputScope.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 }
    )
}

Example of a UI element responding to taps

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))
        }
    }
}

A simple vertical list responding to scroll gestures

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 ScrollableController is required for this modifier to work correctly. When constructing ScrollableController 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. The amount of scrolling distance consumed must be returned from this function to ensure proper event propagation.

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())
    }
}

A UI element detecting the finger press and displaying the numeric value for the finger's location

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.

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)
                )
            }
        }
    }
}

Two nested vertical scrolling UI elements, responding to gestures inside and outside the inner element

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.

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
                }
            }
    )
}

A UI element being dragged by a finger press

Swiping

The swipeablemodifier 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 thresholdshave 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)
        )
    }
}

A UI element responding to a swipe gesture

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()
    )
}

A UI element responding to multitouch gestures—panning, zooming, and rotating

If you need to combine zooming, panning and rotation with other gestures, you can use the PointerInputScope.detectTransformGestures detector.