androidx.compose.foundation.gestures

In this page, you'll find documentation for types, properties, and functions available in the androidx.compose.foundation.gestures package. For example:

If you're looking for guidance instead, check out the Gestures in Compose guide.

Interfaces

AnchoredDragScope

Scope used for suspending anchored drag blocks.

Cmn
BringIntoViewSpec

The configuration of how a scrollable reacts to bring into view requests.

Cmn
Drag2DScope

Scope used for suspending drag blocks

Cmn
DragScope

Scope used for suspending drag blocks

Cmn
Draggable2DState

State of Draggable2D.

Cmn
DraggableAnchors

Structure that represents the anchors of a AnchoredDraggableState.

Cmn
DraggableState

State of draggable.

Cmn
FlingBehavior

Interface to specify fling behavior.

Cmn
PressGestureScope

Receiver scope for detectTapGestures's onPress lambda.

Cmn
ScrollScope

Scope used for suspending scroll blocks

Cmn
ScrollableState

An object representing something that can be scrolled.

Cmn
TargetedFlingBehavior

Interface to specify fling behavior with additional information about its animation target.

Cmn
TransformScope

Scope used for suspending transformation operations

Cmn
TransformableState

State of transformable.

Cmn

Classes

AnchoredDraggableState

State of the anchoredDraggable modifier.

Cmn
DraggableAnchorsConfig

DraggableAnchorsConfig stores a mutable configuration anchors, comprised of values of T and corresponding Float positions.

Cmn

Exceptions

GestureCancellationException

A gesture was canceled and cannot continue, likely because another gesture has taken over the pointer input stream.

Cmn

Objects

ScrollableDefaults

Contains the default values used by scrollable

Cmn

Enums

Orientation

Class to define possible directions in which common gesture modifiers like draggable and scrollable can drag.

Cmn

Top-level functions summary

Draggable2DState

Default implementation of Draggable2DState interface that allows to pass a simple action that will be invoked when the drag occurs.

Cmn
DraggableAnchors<T>

Create a new DraggableAnchors instance using a builder function.

Cmn
DraggableState
DraggableState(onDelta: (Float) -> Unit)

Default implementation of DraggableState interface that allows to pass a simple action that will be invoked when the drag occurs.

Cmn
ScrollableState
ScrollableState(consumeScrollDelta: (Float) -> Float)

Default implementation of ScrollableState interface that contains necessary information about the ongoing fling and provides smooth scrolling capabilities.

Cmn
TransformableState
TransformableState(
    onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
)

Default implementation of TransformableState interface that contains necessary information about the ongoing transformations and provides smooth transformation capabilities.

Cmn
Draggable2DState

Create and remember default implementation of Draggable2DState interface that allows to pass a simple action that will be invoked when the drag occurs.

Cmn
DraggableState

Create and remember default implementation of DraggableState interface that allows to pass a simple action that will be invoked when the drag occurs.

Cmn
ScrollableState
@Composable
rememberScrollableState(consumeScrollDelta: (Float) -> Float)

Create and remember the default implementation of ScrollableState interface that contains necessary information about the ongoing fling and provides smooth scrolling capabilities.

Cmn
TransformableState
@Composable
rememberTransformableState(
    onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
)

Create and remember default implementation of TransformableState interface that contains necessary information about the ongoing transformations and provides smooth transformation capabilities.

Cmn

Extension functions summary

Modifier
@ExperimentalFoundationApi
<T : Any?> Modifier.anchoredDraggable(
    state: AnchoredDraggableState<T>,
    orientation: Orientation,
    enabled: Boolean,
    reverseDirection: Boolean,
    interactionSource: MutableInteractionSource?,
    overscrollEffect: OverscrollEffect?,
    startDragImmediately: Boolean
)

Enable drag gestures between a set of predefined values.

Cmn
suspend Unit
TransformableState.animatePanBy(
    offset: Offset,
    animationSpec: AnimationSpec<Offset>
)

Animate pan by offset Offset in pixels and suspend until its finished

Cmn
suspend Unit
TransformableState.animateRotateBy(
    degrees: Float,
    animationSpec: AnimationSpec<Float>
)

Animate rotate by a ratio of degrees clockwise and suspend until its finished.

Cmn
suspend Float
ScrollableState.animateScrollBy(
    value: Float,
    animationSpec: AnimationSpec<Float>
)

Scroll by value pixels with animation.

Cmn
suspend Unit

Animate to a targetValue.

Cmn
suspend Float
@ExperimentalFoundationApi
<T : Any?> AnchoredDraggableState<T>.animateToWithDecay(
    targetValue: T,
    velocity: Float
)

Attempt to animate using decay Animation to a targetValue.

Cmn
suspend Unit
TransformableState.animateZoomBy(
    zoomFactor: Float,
    animationSpec: AnimationSpec<Float>
)

Animate zoom by a ratio of zoomFactor over the current size and suspend until its finished.

Cmn
suspend PointerInputChange?

Reads pointer input events until a drag is detected or all pointers are up.

Cmn
suspend Unit

Repeatedly calls block to handle gestures.

Cmn
suspend PointerInputChange
AwaitPointerEventScope.awaitFirstDown(
    requireUnconsumed: Boolean,
    pass: PointerEventPass
)

Reads events until the first down is received in the given pass.

Cmn
suspend PointerInputChange?

Reads pointer input events until a horizontal drag is detected or all pointers are up.

Cmn
suspend PointerInputChange?
AwaitPointerEventScope.awaitHorizontalTouchSlopOrCancellation(
    pointerId: PointerId,
    onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
)

Waits for horizontal drag motion to pass touch slop, using pointerId as the pointer to examine.

Cmn
suspend PointerInputChange?

Waits for a long press by examining pointerId.

Cmn
suspend PointerInputChange?
AwaitPointerEventScope.awaitTouchSlopOrCancellation(
    pointerId: PointerId,
    onTouchSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit
)

Waits for drag motion to pass touch slop, using pointerId as the pointer to examine.

Cmn
suspend PointerInputChange?

Reads pointer input events until a vertical drag is detected or all pointers are up.

Cmn
suspend PointerInputChange?
AwaitPointerEventScope.awaitVerticalTouchSlopOrCancellation(
    pointerId: PointerId,
    onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
)

Waits for vertical drag motion to pass touch slop, using pointerId as the pointer to examine.

Cmn
Offset

Returns the centroid of all pointers that are down and were previously down.

Cmn
Float

Returns the average distance from the centroid for all pointers that are currently and were previously down.

Cmn
Offset

Returns the change in the centroid location between the previous and the current pointers that are down.

Cmn
Float

Returns the rotation, in degrees, of the pointers between the PointerInputChange.previousPosition and PointerInputChange.position states.

Cmn
Float

Uses the change of the centroid size between the PointerInputChange.previousPosition and PointerInputChange.position to determine how much zoom was intended.

Cmn
suspend Unit
PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -> Unit,
    onDragEnd: () -> Unit,
    onDragCancel: () -> Unit,
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)

Gesture detector that waits for pointer down and touch slop in any direction and then calls onDrag for each drag event.

Cmn
suspend Unit
PointerInputScope.detectDragGesturesAfterLongPress(
    onDragStart: (Offset) -> Unit,
    onDragEnd: () -> Unit,
    onDragCancel: () -> Unit,
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
)

Gesture detector that waits for pointer down and long press, after which it calls onDrag for each drag event.

Cmn
suspend Unit
PointerInputScope.detectHorizontalDragGestures(
    onDragStart: (Offset) -> Unit,
    onDragEnd: () -> Unit,
    onDragCancel: () -> Unit,
    onHorizontalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
)

Gesture detector that waits for pointer down and touch slop in the horizontal direction and then calls onHorizontalDrag for each horizontal drag event.

Cmn
suspend Unit
PointerInputScope.detectTapGestures(
    onDoubleTap: ((Offset) -> Unit)?,
    onLongPress: ((Offset) -> Unit)?,
    onPress: suspend PressGestureScope.(Offset) -> Unit,
    onTap: ((Offset) -> Unit)?
)

Detects tap, double-tap, and long press gestures and calls onTap, onDoubleTap, and onLongPress, respectively, when detected.

Cmn
suspend Unit
PointerInputScope.detectTransformGestures(
    panZoomLock: Boolean,
    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
)

A gesture detector for rotation, panning, and zoom.

Cmn
suspend Unit
PointerInputScope.detectVerticalDragGestures(
    onDragStart: (Offset) -> Unit,
    onDragEnd: () -> Unit,
    onDragCancel: () -> Unit,
    onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
)

Gesture detector that waits for pointer down and touch slop in the vertical direction and then calls onVerticalDrag for each vertical drag event.

Cmn
suspend Boolean
AwaitPointerEventScope.drag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) -> Unit
)

Reads position change events for pointerId and calls onDrag for every change in position.

Cmn
Modifier
Modifier.draggable(
    state: DraggableState,
    orientation: Orientation,
    enabled: Boolean,
    interactionSource: MutableInteractionSource?,
    startDragImmediately: Boolean,
    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
    onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
    reverseDirection: Boolean
)

Configure touch dragging for the UI element in a single Orientation.

Cmn
Modifier
@ExperimentalFoundationApi
Modifier.draggable2D(
    state: Draggable2DState,
    enabled: Boolean,
    interactionSource: MutableInteractionSource?,
    startDragImmediately: Boolean,
    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit,
    onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
    reverseDirection: Boolean
)

Configure touch dragging for the UI element in both orientations.

Cmn
suspend Unit

This function is deprecated. Use awaitEachGesture instead. forEachGesture() can drop events between gestures.

Cmn
suspend Boolean
AwaitPointerEventScope.horizontalDrag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) -> Unit
)

Reads horizontal position change events for pointerId and calls onDrag for every change in position.

Cmn
suspend Unit

Pan without animation by a offset Offset in pixels and suspend until it's set.

Cmn
suspend Unit

Rotate without animation by a degrees degrees and suspend until it's set.

Cmn
suspend Float

Jump instantly by value pixels.

Cmn
Modifier
Modifier.scrollable(
    state: ScrollableState,
    orientation: Orientation,
    enabled: Boolean,
    reverseDirection: Boolean,
    flingBehavior: FlingBehavior?,
    interactionSource: MutableInteractionSource?
)

Configure touch scrolling and flinging for the UI element in a single Orientation.

Cmn
Modifier
@ExperimentalFoundationApi
Modifier.scrollable(
    state: ScrollableState,
    orientation: Orientation,
    overscrollEffect: OverscrollEffect?,
    enabled: Boolean,
    reverseDirection: Boolean,
    flingBehavior: FlingBehavior?,
    interactionSource: MutableInteractionSource?,
    bringIntoViewSpec: BringIntoViewSpec
)

Configure touch scrolling and flinging for the UI element in a single Orientation.

Cmn
suspend Unit

Snap to a targetValue without any animation.

Cmn
suspend Unit

Stop and suspend until any ongoing animation, smooth scrolling, fling, or any other scroll occurring via ScrollableState.scroll is terminated.

Cmn
suspend Unit

Stop and suspend until any ongoing TransformableState.transform with priority terminationPriority or lower is terminated.

Cmn
Modifier
Modifier.transformable(
    state: TransformableState,
    lockRotationOnZoomPan: Boolean,
    enabled: Boolean
)

Enable transformation gestures of the modified UI element.

Cmn
Modifier
@ExperimentalFoundationApi
Modifier.transformable(
    state: TransformableState,
    canPan: (Offset) -> Boolean,
    lockRotationOnZoomPan: Boolean,
    enabled: Boolean
)

Enable transformation gestures of the modified UI element.

Cmn
suspend Boolean
AwaitPointerEventScope.verticalDrag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) -> Unit
)

Reads vertical position change events for pointerId and calls onDrag for every change in position.

Cmn
suspend PointerInputChange?

Reads events in the given pass until all pointers are up or the gesture was canceled.

Cmn
suspend Unit

Zoom without animation by a ratio of zoomFactor over the current size and suspend until it's set.

Cmn

Top-level functions

Draggable2DState

@ExperimentalFoundationApi
fun Draggable2DState(onDelta: (Offset) -> Unit): Draggable2DState

Default implementation of Draggable2DState interface that allows to pass a simple action that will be invoked when the drag occurs.

This is the simplest way to set up a draggable2D modifier. When constructing this Draggable2DState, you must provide a onDelta lambda, which will be invoked whenever drag happens (by gesture input or a custom Draggable2DState.drag call) with the delta in pixels.

If you are creating Draggable2DState in composition, consider using rememberDraggable2DState.

Parameters
onDelta: (Offset) -> Unit

callback invoked when drag occurs. The callback receives the delta in pixels.

DraggableAnchors

@ExperimentalFoundationApi
fun <T : Any> DraggableAnchors(builder: DraggableAnchorsConfig<T>.() -> Unit): DraggableAnchors<T>

Create a new DraggableAnchors instance using a builder function.

Parameters
builder: DraggableAnchorsConfig<T>.() -> Unit

A function with a DraggableAnchorsConfig that offers APIs to configure anchors

Returns
DraggableAnchors<T>

A new DraggableAnchors instance with the anchor positions set by the builder function.

DraggableState

fun DraggableState(onDelta: (Float) -> Unit): DraggableState

Default implementation of DraggableState interface that allows to pass a simple action that will be invoked when the drag occurs.

This is the simplest way to set up a draggable modifier. When constructing this DraggableState, you must provide a onDelta lambda, which will be invoked whenever drag happens (by gesture input or a custom DraggableState.drag call) with the delta in pixels.

If you are creating DraggableState in composition, consider using rememberDraggableState.

Parameters
onDelta: (Float) -> Unit

callback invoked when drag occurs. The callback receives the delta in pixels.

ScrollableState

fun ScrollableState(consumeScrollDelta: (Float) -> Float): ScrollableState

Default implementation of ScrollableState interface that contains necessary information about the ongoing fling and provides smooth scrolling capabilities.

This is the simplest way to set up a scrollable modifier. When constructing this ScrollableState, you must provide a consumeScrollDelta lambda, which will be invoked whenever scroll happens (by gesture input, by smooth scrolling, by flinging or nested scroll) with the delta in pixels. The amount of scrolling delta consumed must be returned from this lambda to ensure proper nested scrolling behaviour.

Parameters
consumeScrollDelta: (Float) -> Float

callback invoked when drag/fling/smooth scrolling occurs. The callback receives the delta in pixels. Callers should update their state in this lambda and return the amount of delta consumed

TransformableState

fun TransformableState(
    onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
): TransformableState

Default implementation of TransformableState interface that contains necessary information about the ongoing transformations and provides smooth transformation capabilities.

This is the simplest way to set up a transformable modifier. When constructing this TransformableState, you must provide a onTransformation lambda, which will be invoked whenever pan, zoom or rotation happens (by gesture input or any TransformableState.transform call) with the deltas from the previous event.

Parameters
onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit

callback invoked when transformation occurs. The callback receives the change from the previous event. It's relative scale multiplier for zoom, Offset in pixels for pan and degrees for rotation. Callers should update their state in this lambda.

rememberDraggable2DState

@ExperimentalFoundationApi
@Composable
fun rememberDraggable2DState(onDelta: (Offset) -> Unit): Draggable2DState

Create and remember default implementation of Draggable2DState interface that allows to pass a simple action that will be invoked when the drag occurs.

This is the simplest way to set up a draggable modifier. When constructing this Draggable2DState, you must provide a onDelta lambda, which will be invoked whenever drag happens (by gesture input or a custom Draggable2DState.drag call) with the delta in pixels.

Parameters
onDelta: (Offset) -> Unit

callback invoked when drag occurs. The callback receives the delta in pixels.

rememberDraggableState

@Composable
fun rememberDraggableState(onDelta: (Float) -> Unit): DraggableState

Create and remember default implementation of DraggableState interface that allows to pass a simple action that will be invoked when the drag occurs.

This is the simplest way to set up a draggable modifier. When constructing this DraggableState, you must provide a onDelta lambda, which will be invoked whenever drag happens (by gesture input or a custom DraggableState.drag call) with the delta in pixels.

Parameters
onDelta: (Float) -> Unit

callback invoked when drag occurs. The callback receives the delta in pixels.

rememberScrollableState

@Composable
fun rememberScrollableState(consumeScrollDelta: (Float) -> Float): ScrollableState

Create and remember the default implementation of ScrollableState interface that contains necessary information about the ongoing fling and provides smooth scrolling capabilities.

This is the simplest way to set up a scrollable modifier. When constructing this ScrollableState, you must provide a consumeScrollDelta lambda, which will be invoked whenever scroll happens (by gesture input, by smooth scrolling, by flinging or nested scroll) with the delta in pixels. The amount of scrolling delta consumed must be returned from this lambda to ensure proper nested scrolling behaviour.

Parameters
consumeScrollDelta: (Float) -> Float

callback invoked when drag/fling/smooth scrolling occurs. The callback receives the delta in pixels. Callers should update their state in this lambda and return the amount of delta consumed

rememberTransformableState

@Composable
fun rememberTransformableState(
    onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
): TransformableState

Create and remember default implementation of TransformableState interface that contains necessary information about the ongoing transformations and provides smooth transformation capabilities.

This is the simplest way to set up a transformable modifier. When constructing this TransformableState, you must provide a onTransformation lambda, which will be invoked whenever pan, zoom or rotation happens (by gesture input or any TransformableState.transform call) with the deltas from the previous event.

Parameters
onTransformation: (zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit

callback invoked when transformation occurs. The callback receives the change from the previous event. It's relative scale multiplier for zoom, Offset in pixels for pan and degrees for rotation. Callers should update their state in this lambda.

Extension functions

anchoredDraggable

@ExperimentalFoundationApi
fun <T : Any?> Modifier.anchoredDraggable(
    state: AnchoredDraggableState<T>,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    interactionSource: MutableInteractionSource? = null,
    overscrollEffect: OverscrollEffect? = null,
    startDragImmediately: Boolean = state.isAnimationRunning
): Modifier

Enable drag gestures between a set of predefined values.

When a drag is detected, the offset of the AnchoredDraggableState will be updated with the drag delta. You should use this offset to move your content accordingly (see Modifier.offset). When the drag ends, the offset will be animated to one of the anchors and when that anchor is reached, the value of the AnchoredDraggableState will also be updated to the value corresponding to the new anchor.

Dragging is constrained between the minimum and maximum anchors.

Parameters
state: AnchoredDraggableState<T>

The associated AnchoredDraggableState.

orientation: Orientation

The orientation in which the anchoredDraggable can be dragged.

enabled: Boolean = true

Whether this anchoredDraggable is enabled and should react to the user's input.

reverseDirection: Boolean = false

Whether to reverse the direction of the drag, so a top to bottom drag will behave like bottom to top, and a left to right drag will behave like right to left.

interactionSource: MutableInteractionSource? = null

Optional MutableInteractionSource that will passed on to the internal Modifier.draggable.

overscrollEffect: OverscrollEffect? = null

optional effect to dispatch any excess delta or velocity to. The excess delta or velocity are a result of dragging/flinging and reaching the bounds. If you provide an overscrollEffect, make sure to apply androidx.compose.foundation.overscroll to render the effect as well.

startDragImmediately: Boolean = state.isAnimationRunning

when set to false, draggable will start dragging only when the gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating widget when pressing on it. See draggable to learn more about startDragImmediately.

suspend fun TransformableState.animatePanBy(
    offset: Offset,
    animationSpec: AnimationSpec<Offset> = SpringSpec(stiffness = Spring.StiffnessLow)
): Unit

Animate pan by offset Offset in pixels and suspend until its finished

Parameters
offset: Offset

offset to pan, in pixels

animationSpec: AnimationSpec<Offset> = SpringSpec(stiffness = Spring.StiffnessLow)

AnimationSpec to be used for pan animation

animateRotateBy

suspend fun TransformableState.animateRotateBy(
    degrees: Float,
    animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)
): Unit

Animate rotate by a ratio of degrees clockwise and suspend until its finished.

Parameters
degrees: Float

ratio over the current size by which to rotate, in degrees

animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)

AnimationSpec to be used for animation

animateScrollBy

suspend fun ScrollableState.animateScrollBy(
    value: Float,
    animationSpec: AnimationSpec<Float> = spring()
): Float

Scroll by value pixels with animation.

Cancels the currently running scroll, if any, and suspends until the cancellation is complete.

Parameters
value: Float

number of pixels to scroll by

animationSpec: AnimationSpec<Float> = spring()

AnimationSpec to be used for this scrolling

Returns
Float

the amount of scroll consumed

@ExperimentalFoundationApi
suspend fun <T : Any?> AnchoredDraggableState<T>.animateTo(targetValue: T): Unit

Animate to a targetValue. If the targetValue is not in the set of anchors, the AnchoredDraggableState.currentValue will be updated to the targetValue without updating the offset.

Parameters
targetValue: T

The target value of the animation

Throws
kotlinx.coroutines.CancellationException

if the interaction interrupted by another interaction like a gesture interaction or another programmatic interaction like a animateTo or snapTo call.

animateToWithDecay

@ExperimentalFoundationApi
suspend fun <T : Any?> AnchoredDraggableState<T>.animateToWithDecay(
    targetValue: T,
    velocity: Float
): Float

Attempt to animate using decay Animation to a targetValue. If the velocity is high enough to get to the target offset, we'll use AnchoredDraggableState.decayAnimationSpec to get to that offset and return the consumed velocity. If the velocity is not high enough, we'll use AnchoredDraggableState.snapAnimationSpec to reach the target offset.

If the targetValue is not in the set of anchors, AnchoredDraggableState.currentValue will be updated ro the targetValue without updating the offset.

Parameters
targetValue: T

The target value of the animation

velocity: Float

The velocity the animation should start with

Returns
Float

The velocity consumed in the animation

Throws
kotlinx.coroutines.CancellationException

if the interaction interrupted bt another interaction like a gesture interaction or another programmatic interaction like animateTo or snapTo call.

animateZoomBy

suspend fun TransformableState.animateZoomBy(
    zoomFactor: Float,
    animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)
): Unit

Animate zoom by a ratio of zoomFactor over the current size and suspend until its finished.

Parameters
zoomFactor: Float

ratio over the current size by which to zoom. For example, if zoomFactor is 3f, zoom will be increased 3 fold from the current value.

animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)

AnimationSpec to be used for animation

awaitDragOrCancellation

suspend fun AwaitPointerEventScope.awaitDragOrCancellation(pointerId: PointerId): PointerInputChange?

Reads pointer input events until a drag is detected or all pointers are up. When the final pointer is raised, the up event is returned. When a drag event is detected, the drag change will be returned. Note that if pointerId has been raised, another pointer that is down will be used, if available, so the returned PointerInputChange.id may differ from pointerId. If the position change in the any direction has been consumed by the PointerEventPass.Main pass, then the drag is considered canceled and null is returned. If pointerId is not down when awaitDragOrCancellation is called, then null is returned.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitDragOrCancellation
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.toSize

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var size by remember { mutableStateOf(Size.Zero) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { size = it.toSize() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .size(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    var change = awaitTouchSlopOrCancellation(down.id) { change, over ->
                        val original = Offset(offsetX.value, offsetY.value)
                        val summed = original + over
                        val newValue = Offset(
                            x = summed.x.coerceIn(0f, size.width - 50.dp.toPx()),
                            y = summed.y.coerceIn(0f, size.height - 50.dp.toPx())
                        )
                        change.consume()
                        offsetX.value = newValue.x
                        offsetY.value = newValue.y
                    }
                    while (change != null && change.pressed) {
                        change = awaitDragOrCancellation(change.id)
                        if (change != null && change.pressed) {
                            val original = Offset(offsetX.value, offsetY.value)
                            val summed = original + change.positionChange()
                            val newValue = Offset(
                                x = summed.x.coerceIn(0f, size.width - 50.dp.toPx()),
                                y = summed.y.coerceIn(0f, size.height - 50.dp.toPx())
                            )
                            change.consume()
                            offsetX.value = newValue.x
                            offsetY.value = newValue.y
                        }
                    }
                }
            }
    )
}

awaitEachGesture

suspend fun PointerInputScope.awaitEachGesture(block: suspend AwaitPointerEventScope.() -> Unit): Unit

Repeatedly calls block to handle gestures. If there is a CancellationException, it will wait until all pointers are raised before another gesture is detected, or it exits if isActive is false.

block is run within PointerInputScope.awaitPointerEventScope and will loop entirely within the AwaitPointerEventScope so events will not be lost between gestures.

suspend fun AwaitPointerEventScope.awaitFirstDown(
    requireUnconsumed: Boolean = true,
    pass: PointerEventPass = PointerEventPass.Main
): PointerInputChange

Reads events until the first down is received in the given pass. If requireUnconsumed is true and the first down is already consumed in the pass, that gesture is ignored.

awaitHorizontalDragOrCancellation

suspend fun AwaitPointerEventScope.awaitHorizontalDragOrCancellation(
    pointerId: PointerId
): PointerInputChange?

Reads pointer input events until a horizontal drag is detected or all pointers are up. When the final pointer is raised, the up event is returned. When a drag event is detected, the drag change will be returned. Note that if pointerId has been raised, another pointer that is down will be used, if available, so the returned PointerInputChange.id may differ from pointerId. If the position change has been consumed by the PointerEventPass.Main pass, then the drag is considered canceled and null is returned. If pointerId is not down when awaitHorizontalDragOrCancellation is called, then null is returned.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitHorizontalDragOrCancellation
import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var width by remember { mutableStateOf(0f) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { width = it.width.toFloat() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .fillMaxHeight()
            .width(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    var change =
                        awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
                            val originalX = offsetX.value
                            val newValue =
                                (originalX + over).coerceIn(0f, width - 50.dp.toPx())
                            change.consume()
                            offsetX.value = newValue
                        }
                    while (change != null && change.pressed) {
                        change = awaitHorizontalDragOrCancellation(change.id)
                        if (change != null && change.pressed) {
                            val originalX = offsetX.value
                            val newValue = (originalX + change.positionChange().x)
                                .coerceIn(0f, width - 50.dp.toPx())
                            change.consume()
                            offsetX.value = newValue
                        }
                    }
                }
            }
    )
}

awaitHorizontalTouchSlopOrCancellation

suspend fun AwaitPointerEventScope.awaitHorizontalTouchSlopOrCancellation(
    pointerId: PointerId,
    onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
): PointerInputChange?

Waits for horizontal drag motion to pass touch slop, using pointerId as the pointer to examine. If pointerId is raised, another pointer from those that are down will be chosen to lead the gesture, and if none are down, null is returned.

onTouchSlopReached is called after ViewConfiguration.touchSlop motion in the horizontal direction with the change that caused the motion beyond touch slop and the pixels beyond touch slop. onTouchSlopReached should consume the position change if it accepts the motion. If it does, then the method returns that PointerInputChange. If not, touch slop detection will continue. If pointerId is not down when awaitHorizontalTouchSlopOrCancellation is called, then null is returned.

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitHorizontalDragOrCancellation
import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var width by remember { mutableStateOf(0f) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { width = it.width.toFloat() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .fillMaxHeight()
            .width(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    var change =
                        awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
                            val originalX = offsetX.value
                            val newValue =
                                (originalX + over).coerceIn(0f, width - 50.dp.toPx())
                            change.consume()
                            offsetX.value = newValue
                        }
                    while (change != null && change.pressed) {
                        change = awaitHorizontalDragOrCancellation(change.id)
                        if (change != null && change.pressed) {
                            val originalX = offsetX.value
                            val newValue = (originalX + change.positionChange().x)
                                .coerceIn(0f, width - 50.dp.toPx())
                            change.consume()
                            offsetX.value = newValue
                        }
                    }
                }
            }
    )
}
Returns
PointerInputChange?

The PointerInputChange that was consumed in onTouchSlopReached or null if all pointers are raised before touch slop is detected or another gesture consumed the position change.

Example Usage:

awaitLongPressOrCancellation

suspend fun AwaitPointerEventScope.awaitLongPressOrCancellation(
    pointerId: PointerId
): PointerInputChange?

Waits for a long press by examining pointerId.

If that pointerId is raised (that is, the user lifts their finger), but another finger (PointerId) is down at that time, another pointer will be chosen as the lead for the gesture, and if none are down, null is returned.

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitLongPressOrCancellation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.input.pointer.pointerInput

var count by remember { mutableStateOf(0) }

Column {
    Text("Long Press to increase count. Long Press count: $count")
    Box(
        Modifier.fillMaxSize()
            .wrapContentSize(Alignment.Center)
            .size(192.dp)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown(requireUnconsumed = false)
                    awaitLongPressOrCancellation(down.id)?.let {
                        count++
                    }
                }
            }
            .clipToBounds()
            .background(Color.Blue)
            .border(BorderStroke(2.dp, Color.Black))
    )
}
Returns
PointerInputChange?

The latest PointerInputChange associated with a long press or null if all pointers are raised before a long press is detected or another gesture consumed the change.

Example Usage:

awaitTouchSlopOrCancellation

suspend fun AwaitPointerEventScope.awaitTouchSlopOrCancellation(
    pointerId: PointerId,
    onTouchSlopReached: (change: PointerInputChange, overSlop: Offset) -> Unit
): PointerInputChange?

Waits for drag motion to pass touch slop, using pointerId as the pointer to examine. If pointerId is raised, another pointer from those that are down will be chosen to lead the gesture, and if none are down, null is returned. If pointerId is not down when awaitTouchSlopOrCancellation is called, then null is returned.

onTouchSlopReached is called after ViewConfiguration.touchSlop motion in the any direction with the change that caused the motion beyond touch slop and the Offset beyond touch slop that has passed. onTouchSlopReached should consume the position change if it accepts the motion. If it does, then the method returns that PointerInputChange. If not, touch slop detection will continue.

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitDragOrCancellation
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.toSize

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var size by remember { mutableStateOf(Size.Zero) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { size = it.toSize() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .size(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    var change = awaitTouchSlopOrCancellation(down.id) { change, over ->
                        val original = Offset(offsetX.value, offsetY.value)
                        val summed = original + over
                        val newValue = Offset(
                            x = summed.x.coerceIn(0f, size.width - 50.dp.toPx()),
                            y = summed.y.coerceIn(0f, size.height - 50.dp.toPx())
                        )
                        change.consume()
                        offsetX.value = newValue.x
                        offsetY.value = newValue.y
                    }
                    while (change != null && change.pressed) {
                        change = awaitDragOrCancellation(change.id)
                        if (change != null && change.pressed) {
                            val original = Offset(offsetX.value, offsetY.value)
                            val summed = original + change.positionChange()
                            val newValue = Offset(
                                x = summed.x.coerceIn(0f, size.width - 50.dp.toPx()),
                                y = summed.y.coerceIn(0f, size.height - 50.dp.toPx())
                            )
                            change.consume()
                            offsetX.value = newValue.x
                            offsetY.value = newValue.y
                        }
                    }
                }
            }
    )
}
Returns
PointerInputChange?

The PointerInputChange that was consumed in onTouchSlopReached or null if all pointers are raised before touch slop is detected or another gesture consumed the position change.

Example Usage:

awaitVerticalDragOrCancellation

suspend fun AwaitPointerEventScope.awaitVerticalDragOrCancellation(
    pointerId: PointerId
): PointerInputChange?

Reads pointer input events until a vertical drag is detected or all pointers are up. When the final pointer is raised, the up event is returned. When a drag event is detected, the drag change will be returned. Note that if pointerId has been raised, another pointer that is down will be used, if available, so the returned PointerInputChange.id may differ from pointerId. If the position change has been consumed by the PointerEventPass.Main pass, then the drag is considered canceled and null is returned. If pointerId is not down when awaitVerticalDragOrCancellation is called, then null is returned.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitVerticalDragOrCancellation
import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var height by remember { mutableStateOf(0f) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { height = it.height.toFloat() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .fillMaxWidth()
            .height(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    var change =
                        awaitVerticalTouchSlopOrCancellation(down.id) { change, over ->
                            val originalY = offsetY.value
                            val newValue = (originalY + over)
                                .coerceIn(0f, height - 50.dp.toPx())
                            change.consume()
                            offsetY.value = newValue
                        }
                    while (change != null && change.pressed) {
                        change = awaitVerticalDragOrCancellation(change.id)
                        if (change != null && change.pressed) {
                            val originalY = offsetY.value
                            val newValue = (originalY + change.positionChange().y)
                                .coerceIn(0f, height - 50.dp.toPx())
                            change.consume()
                            offsetY.value = newValue
                        }
                    }
                }
            }
    )
}

awaitVerticalTouchSlopOrCancellation

suspend fun AwaitPointerEventScope.awaitVerticalTouchSlopOrCancellation(
    pointerId: PointerId,
    onTouchSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
): PointerInputChange?

Waits for vertical drag motion to pass touch slop, using pointerId as the pointer to examine. If pointerId is raised, another pointer from those that are down will be chosen to lead the gesture, and if none are down, null is returned. If pointerId is not down when awaitVerticalTouchSlopOrCancellation is called, then null is returned.

onTouchSlopReached is called after ViewConfiguration.touchSlop motion in the vertical direction with the change that caused the motion beyond touch slop and the pixels beyond touch slop. onTouchSlopReached should consume the position change if it accepts the motion. If it does, then the method returns that PointerInputChange. If not, touch slop detection will continue.

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitVerticalDragOrCancellation
import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var height by remember { mutableStateOf(0f) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { height = it.height.toFloat() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .fillMaxWidth()
            .height(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    var change =
                        awaitVerticalTouchSlopOrCancellation(down.id) { change, over ->
                            val originalY = offsetY.value
                            val newValue = (originalY + over)
                                .coerceIn(0f, height - 50.dp.toPx())
                            change.consume()
                            offsetY.value = newValue
                        }
                    while (change != null && change.pressed) {
                        change = awaitVerticalDragOrCancellation(change.id)
                        if (change != null && change.pressed) {
                            val originalY = offsetY.value
                            val newValue = (originalY + change.positionChange().y)
                                .coerceIn(0f, height - 50.dp.toPx())
                            change.consume()
                            offsetY.value = newValue
                        }
                    }
                }
            }
    )
}
Returns
PointerInputChange?

The PointerInputChange that was consumed in onTouchSlopReached or null if all pointers are raised before touch slop is detected or another gesture consumed the position change.

Example Usage:

calculateCentroid

fun PointerEvent.calculateCentroid(useCurrent: Boolean = true): Offset

Returns the centroid of all pointers that are down and were previously down. If no pointers are down, Offset.Unspecified is returned. If useCurrent is true, the centroid of the PointerInputChange.position is returned and if false, the centroid of the PointerInputChange.previousPosition is returned. Only pointers that are down in both the previous and current state are used to calculate the centroid.

Example Usage:

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.calculateCentroid
import androidx.compose.foundation.gestures.calculateCentroidSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.input.pointer.pointerInput

var centroidSize by remember { mutableStateOf(0f) }
var position by remember { mutableStateOf(Offset.Zero) }
Box(
    Modifier
        .drawBehind {
            // Draw a circle where the gesture is
            drawCircle(Color.Blue, centroidSize, center = position)
        }
        .pointerInput(Unit) {
            awaitEachGesture {
                awaitFirstDown().also {
                    position = it.position
                }
                do {
                    val event = awaitPointerEvent()
                    val size = event.calculateCentroidSize()
                    if (size != 0f) {
                        centroidSize = event.calculateCentroidSize()
                    }
                    val centroid = event.calculateCentroid()
                    if (centroid != Offset.Unspecified) {
                        position = centroid
                    }
                } while (event.changes.any { it.pressed })
            }
        }
        .fillMaxSize()
)

calculateCentroidSize

fun PointerEvent.calculateCentroidSize(useCurrent: Boolean = true): Float

Returns the average distance from the centroid for all pointers that are currently and were previously down. If no pointers are down, 0 is returned. If useCurrent is true, the size of the PointerInputChange.position is returned and if false, the size of PointerInputChange.previousPosition is returned. Only pointers that are down in both the previous and current state are used to calculate the centroid size.

Example Usage:

import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.calculateCentroid
import androidx.compose.foundation.gestures.calculateCentroidSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.input.pointer.pointerInput

var centroidSize by remember { mutableStateOf(0f) }
var position by remember { mutableStateOf(Offset.Zero) }
Box(
    Modifier
        .drawBehind {
            // Draw a circle where the gesture is
            drawCircle(Color.Blue, centroidSize, center = position)
        }
        .pointerInput(Unit) {
            awaitEachGesture {
                awaitFirstDown().also {
                    position = it.position
                }
                do {
                    val event = awaitPointerEvent()
                    val size = event.calculateCentroidSize()
                    if (size != 0f) {
                        centroidSize = event.calculateCentroidSize()
                    }
                    val centroid = event.calculateCentroid()
                    if (centroid != Offset.Unspecified) {
                        position = centroid
                    }
                } while (event.changes.any { it.pressed })
            }
        }
        .fillMaxSize()
)

calculatePan

fun PointerEvent.calculatePan(): Offset

Returns the change in the centroid location between the previous and the current pointers that are down. Pointers that are newly down or raised are not considered in the centroid movement.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.calculatePan
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
Box(
    Modifier
        .offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
        .graphicsLayer()
        .background(Color.Blue)
        .pointerInput(Unit) {
            awaitEachGesture {
                awaitFirstDown()
                do {
                    val event = awaitPointerEvent()
                    val offset = event.calculatePan()
                    offsetX.value += offset.x
                    offsetY.value += offset.y
                } while (event.changes.any { it.pressed })
            }
        }
        .fillMaxSize()
)

calculateRotation

fun PointerEvent.calculateRotation(): Float

Returns the rotation, in degrees, of the pointers between the PointerInputChange.previousPosition and PointerInputChange.position states. Only the pointers that are down in both previous and current states are considered.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.calculateRotation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput

var angle by remember { mutableStateOf(0f) }
Box(
    Modifier
        .graphicsLayer(rotationZ = angle)
        .background(Color.Blue)
        .pointerInput(Unit) {
            awaitEachGesture {
                awaitFirstDown()
                do {
                    val event = awaitPointerEvent()
                    val rotation = event.calculateRotation()
                    angle += rotation
                } while (event.changes.any { it.pressed })
            }
        }
        .fillMaxSize()
)

calculateZoom

fun PointerEvent.calculateZoom(): Float

Uses the change of the centroid size between the PointerInputChange.previousPosition and PointerInputChange.position to determine how much zoom was intended.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.calculateZoom
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput

var zoom by remember { mutableStateOf(1f) }
Box(
    Modifier
        .graphicsLayer(scaleX = zoom, scaleY = zoom)
        .background(Color.Blue)
        .pointerInput(Unit) {
            awaitEachGesture {
                awaitFirstDown()
                do {
                    val event = awaitPointerEvent()
                    zoom *= event.calculateZoom()
                } while (event.changes.any { it.pressed })
            }
        }
        .fillMaxSize()
)

detectDragGestures

suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
): Unit

Gesture detector that waits for pointer down and touch slop in any direction and then calls onDrag for each drag event. It follows the touch slop detection of awaitTouchSlopOrCancellation but will consume the position change automatically once the touch slop has been crossed.

onDragStart called when the touch slop has been passed and includes an Offset representing the last known pointer position relative to the containing element. The Offset can be outside the actual bounds of the element itself meaning the numbers can be negative or larger than the element bounds if the touch target is smaller than the ViewConfiguration.minimumTouchTargetSize.

onDragEnd is called after all pointers are up and onDragCancel is called if another gesture has consumed pointer input, canceling this gesture.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.toSize

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var size by remember { mutableStateOf(Size.Zero) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { size = it.toSize() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .size(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                detectDragGestures { _, dragAmount ->
                    val original = Offset(offsetX.value, offsetY.value)
                    val summed = original + dragAmount
                    val newValue = Offset(
                        x = summed.x.coerceIn(0f, size.width - 50.dp.toPx()),
                        y = summed.y.coerceIn(0f, size.height - 50.dp.toPx())
                    )
                    offsetX.value = newValue.x
                    offsetY.value = newValue.y
                }
            }
    )
}

detectDragGesturesAfterLongPress

suspend fun PointerInputScope.detectDragGesturesAfterLongPress(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
): Unit

Gesture detector that waits for pointer down and long press, after which it calls onDrag for each drag event.

onDragStart called when a long press is detected and includes an Offset representing the last known pointer position relative to the containing element. The Offset can be outside the actual bounds of the element itself meaning the numbers can be negative or larger than the element bounds if the touch target is smaller than the ViewConfiguration.minimumTouchTargetSize.

onDragEnd is called after all pointers are up and onDragCancel is called if another gesture has consumed pointer input, canceling this gesture. This function will automatically consume all the position change after the long press.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.toSize

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var size by remember { mutableStateOf(Size.Zero) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { size = it.toSize() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .size(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                detectDragGesturesAfterLongPress { _, dragAmount ->
                    val original = Offset(offsetX.value, offsetY.value)
                    val summed = original + dragAmount
                    val newValue = Offset(
                        x = summed.x.coerceIn(0f, size.width - 50.dp.toPx()),
                        y = summed.y.coerceIn(0f, size.height - 50.dp.toPx())
                    )
                    offsetX.value = newValue.x
                    offsetY.value = newValue.y
                }
            }
    )
}

detectHorizontalDragGestures

suspend fun PointerInputScope.detectHorizontalDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onHorizontalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
): Unit

Gesture detector that waits for pointer down and touch slop in the horizontal direction and then calls onHorizontalDrag for each horizontal drag event. It follows the touch slop detection of awaitHorizontalTouchSlopOrCancellation, but will consume the position change automatically once the touch slop has been crossed.

onDragStart called when the touch slop has been passed and includes an Offset representing the last known pointer position relative to the containing element. The Offset can be outside the actual bounds of the element itself meaning the numbers can be negative or larger than the element bounds if the touch target is smaller than the ViewConfiguration.minimumTouchTargetSize.

onDragEnd is called after all pointers are up and onDragCancel is called if another gesture has consumed pointer input, canceling this gesture.

This gesture detector will coordinate with detectVerticalDragGestures and awaitVerticalTouchSlopOrCancellation to ensure only vertical or horizontal dragging is locked, but not both.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var width by remember { mutableStateOf(0f) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { width = it.width.toFloat() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .fillMaxHeight()
            .width(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                detectHorizontalDragGestures { _, dragAmount ->
                    val originalX = offsetX.value
                    val newValue = (originalX + dragAmount).coerceIn(0f, width - 50.dp.toPx())
                    offsetX.value = newValue
                }
            }
    )
}

detectTapGestures

suspend fun PointerInputScope.detectTapGestures(
    onDoubleTap: ((Offset) -> Unit)? = null,
    onLongPress: ((Offset) -> Unit)? = null,
    onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
    onTap: ((Offset) -> Unit)? = null
): Unit

Detects tap, double-tap, and long press gestures and calls onTap, onDoubleTap, and onLongPress, respectively, when detected. onPress is called when the press is detected and the PressGestureScope.tryAwaitRelease and PressGestureScope.awaitRelease can be used to detect when pointers have released or the gesture was canceled. The first pointer down and final pointer up are consumed, and in the case of long press, all changes after the long press is detected are consumed.

Each function parameter receives an Offset representing the position relative to the containing element. The Offset can be outside the actual bounds of the element itself meaning the numbers can be negative or larger than the element bounds if the touch target is smaller than the ViewConfiguration.minimumTouchTargetSize.

When onDoubleTap is provided, the tap gesture is detected only after the ViewConfiguration.doubleTapMinTimeMillis has passed and onDoubleTap is called if the second tap is started before ViewConfiguration.doubleTapTimeoutMillis. If onDoubleTap is not provided, then onTap is called when the pointer up has been received.

After the initial onPress, if the pointer moves out of the input area, the position change is consumed, or another gesture consumes the down or up events, the gestures are considered canceled. That means onDoubleTap, onLongPress, and onTap will not be called after a gesture has been canceled.

If the first down event is consumed somewhere else, the entire gesture will be skipped, including onPress.

detectTransformGestures

suspend fun PointerInputScope.detectTransformGestures(
    panZoomLock: Boolean = false,
    onGesture: (centroid: Offset, pan: Offset, zoom: Float, rotation: Float) -> Unit
): Unit

A gesture detector for rotation, panning, and zoom. Once touch slop has been reached, the user can use rotation, panning and zoom gestures. onGesture will be called when any of the rotation, zoom or pan occurs, passing the rotation angle in degrees, zoom in scale factor and pan as an offset in pixels. Each of these changes is a difference between the previous call and the current gesture. This will consume all position changes after touch slop has been reached. onGesture will also provide centroid of all the pointers that are down.

If panZoomLock is true, rotation is allowed only if touch slop is detected for rotation before pan or zoom motions. If not, pan and zoom gestures will be detected, but rotation gestures will not be. If panZoomLock is false, once touch slop is reached, all three gestures are detected.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput

/**
 * Rotates the given offset around the origin by the given angle in degrees.
 *
 * A positive angle indicates a counterclockwise rotation around the right-handed 2D Cartesian
 * coordinate system.
 *
 * See: [Rotation matrix](https://en.wikipedia.org/wiki/Rotation_matrix)
 */
fun Offset.rotateBy(angle: Float): Offset {
    val angleInRadians = angle * PI / 180
    return Offset(
        (x * cos(angleInRadians) - y * sin(angleInRadians)).toFloat(),
        (x * sin(angleInRadians) + y * cos(angleInRadians)).toFloat()
    )
}

var offset by remember { mutableStateOf(Offset.Zero) }
var zoom by remember { mutableStateOf(1f) }
var angle by remember { mutableStateOf(0f) }

Box(
    Modifier
        .pointerInput(Unit) {
            detectTransformGestures(
                onGesture = { centroid, pan, gestureZoom, gestureRotate ->
                    val oldScale = zoom
                    val newScale = zoom * gestureZoom

                    // For natural zooming and rotating, the centroid of the gesture should
                    // be the fixed point where zooming and rotating occurs.
                    // We compute where the centroid was (in the pre-transformed coordinate
                    // space), and then compute where it will be after this delta.
                    // We then compute what the new offset should be to keep the centroid
                    // visually stationary for rotating and zooming, and also apply the pan.
                    offset = (offset + centroid / oldScale).rotateBy(gestureRotate) -
                        (centroid / newScale + pan / oldScale)
                    zoom = newScale
                    angle += gestureRotate
                }
            )
        }
        .graphicsLayer {
            translationX = -offset.x * zoom
            translationY = -offset.y * zoom
            scaleX = zoom
            scaleY = zoom
            rotationZ = angle
            transformOrigin = TransformOrigin(0f, 0f)
        }
        .background(Color.Blue)
        .fillMaxSize()
)

detectVerticalDragGestures

suspend fun PointerInputScope.detectVerticalDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit
): Unit

Gesture detector that waits for pointer down and touch slop in the vertical direction and then calls onVerticalDrag for each vertical drag event. It follows the touch slop detection of awaitVerticalTouchSlopOrCancellation, but will consume the position change automatically once the touch slop has been crossed.

onDragStart called when the touch slop has been passed and includes an Offset representing the last known pointer position relative to the containing element. The Offset can be outside the actual bounds of the element itself meaning the numbers can be negative or larger than the element bounds if the touch target is smaller than the ViewConfiguration.minimumTouchTargetSize.

onDragEnd is called after all pointers are up and onDragCancel is called if another gesture has consumed pointer input, canceling this gesture.

This gesture detector will coordinate with detectHorizontalDragGestures and awaitHorizontalTouchSlopOrCancellation to ensure only vertical or horizontal dragging is locked, but not both.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var height by remember { mutableStateOf(0f) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { height = it.height.toFloat() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .fillMaxWidth()
            .height(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                detectVerticalDragGestures { _, dragAmount ->
                    val originalY = offsetY.value
                    val newValue = (originalY + dragAmount).coerceIn(0f, height - 50.dp.toPx())
                    offsetY.value = newValue
                }
            }
    )
}
suspend fun AwaitPointerEventScope.drag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) -> Unit
): Boolean

Reads position change events for pointerId and calls onDrag for every change in position. If pointerId is raised, a new pointer is chosen from those that are down and if none exist, the method returns. This does not wait for touch slop.

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitTouchSlopOrCancellation
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.toSize

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var size by remember { mutableStateOf(Size.Zero) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { size = it.toSize() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .size(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    val change = awaitTouchSlopOrCancellation(down.id) { change, over ->
                        val original = Offset(offsetX.value, offsetY.value)
                        val summed = original + over
                        val newValue = Offset(
                            x = summed.x.coerceIn(0f, size.width - 50.dp.toPx()),
                            y = summed.y.coerceIn(0f, size.height - 50.dp.toPx())
                        )
                        change.consume()
                        offsetX.value = newValue.x
                        offsetY.value = newValue.y
                    }
                    if (change != null) {
                        drag(change.id) {
                            val original = Offset(offsetX.value, offsetY.value)
                            val summed = original + it.positionChange()
                            val newValue = Offset(
                                x = summed.x.coerceIn(0f, size.width - 50.dp.toPx()),
                                y = summed.y.coerceIn(0f, size.height - 50.dp.toPx())
                            )
                            it.consume()
                            offsetX.value = newValue.x
                            offsetY.value = newValue.y
                        }
                    }
                }
            }
    )
}
Returns
Boolean

true if the drag completed normally or false if the drag motion was canceled by another gesture detector consuming position change events.

Example Usage:

draggable

fun Modifier.draggable(
    state: DraggableState,
    orientation: Orientation,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    startDragImmediately: Boolean = false,
    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = NoOpOnDragStarted,
    onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = NoOpOnDragStopped,
    reverseDirection: Boolean = false
): Modifier

Configure touch dragging for the UI element in a single Orientation. The drag distance reported to DraggableState, allowing users to react on the drag delta and update their state.

The common usecase for this component is when you need to be able to drag something inside the component on the screen and represent this state via one float value

If you need to control the whole dragging flow, consider using pointerInput instead with the helper functions like detectDragGestures.

If you want to enable dragging in 2 dimensions, consider using draggable2D.

If you are implementing scroll/fling behavior, consider using scrollable.

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.IntOffset

// Draw a seekbar-like composable that has a black background
// with a red square that moves along the 300.dp drag distance
val max = 300.dp
val min = 0.dp
val (minPx, maxPx) = with(LocalDensity.current) { min.toPx() to max.toPx() }
// this is the  state we will update while dragging
val offsetPosition = remember { mutableStateOf(0f) }

// seekbar itself
Box(
    modifier = Modifier
        .width(max)
        .draggable(
            orientation = Orientation.Horizontal,
            state = rememberDraggableState { delta ->
                val newValue = offsetPosition.value + delta
                offsetPosition.value = newValue.coerceIn(minPx, maxPx)
            }
        )
        .background(Color.Black)
) {
    Box(
        Modifier.offset { IntOffset(offsetPosition.value.roundToInt(), 0) }
            .size(50.dp)
            .background(Color.Red)
    )
}
Parameters
state: DraggableState

DraggableState state of the draggable. Defines how drag events will be interpreted by the user land logic.

orientation: Orientation

orientation of the drag

enabled: Boolean = true

whether or not drag is enabled

interactionSource: MutableInteractionSource? = null

MutableInteractionSource that will be used to emit DragInteraction.Start when this draggable is being dragged.

startDragImmediately: Boolean = false

when set to true, draggable will start dragging immediately and prevent other gesture detectors from reacting to "down" events (in order to block composed press-based gestures). This is intended to allow end users to "catch" an animating widget by pressing on it. It's useful to set it when value you're dragging is settling / animating.

onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = NoOpOnDragStarted

callback that will be invoked when drag is about to start at the starting position, allowing user to suspend and perform preparation for drag, if desired. This suspend function is invoked with the draggable scope, allowing for async processing, if desired. Note that the scope used here is the one provided by the draggable node, for long running work that needs to outlast the modifier being in the composition you should use a scope that fits the lifecycle needed.

onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = NoOpOnDragStopped

callback that will be invoked when drag is finished, allowing the user to react on velocity and process it. This suspend function is invoked with the draggable scope, allowing for async processing, if desired. Note that the scope used here is the one provided by the draggable node, for long running work that needs to outlast the modifier being in the composition you should use a scope that fits the lifecycle needed.

reverseDirection: Boolean = false

reverse the direction of the scroll, so top to bottom scroll will behave like bottom to top and left to right will behave like right to left.

draggable2D

@ExperimentalFoundationApi
fun Modifier.draggable2D(
    state: Draggable2DState,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    startDragImmediately: Boolean = false,
    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = NoOpOnDragStarted,
    onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit = NoOpOnDragStopped,
    reverseDirection: Boolean = false
): Modifier

Configure touch dragging for the UI element in both orientations. The drag distance reported to Draggable2DState, allowing users to react to the drag delta and update their state.

The common common usecase for this component is when you need to be able to drag something inside the component on the screen and represent this state via one float value

If you are implementing dragging in a single orientation, consider using draggable.

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.draggable2D
import androidx.compose.foundation.gestures.rememberDraggable2DState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.unit.IntOffset

// Draw a box that has a a grey background
// with a red square that moves along 300.dp dragging in both directions
val max = 200.dp
val min = 0.dp
val (minPx, maxPx) = with(LocalDensity.current) { min.toPx() to max.toPx() }
// this is the offset we will update while dragging
var offsetPositionX by remember { mutableStateOf(0f) }
var offsetPositionY by remember { mutableStateOf(0f) }

Box(
    modifier = Modifier
        .width(max)
        .height(max)
        .draggable2D(
            state = rememberDraggable2DState { delta ->
                val newValueX = offsetPositionX + delta.x
                val newValueY = offsetPositionY + delta.y
                offsetPositionX = newValueX.coerceIn(minPx, maxPx)
                offsetPositionY = newValueY.coerceIn(minPx, maxPx)
            }
        )
        .background(Color.LightGray)
) {
    Box(
        Modifier
            .offset {
                IntOffset(
                    offsetPositionX.roundToInt(),
                    offsetPositionY.roundToInt()
                )
            }
            .size(50.dp)
            .background(Color.Red)
    )
}
Parameters
state: Draggable2DState

Draggable2DState state of the draggable2D. Defines how drag events will be interpreted by the user land logic.

enabled: Boolean = true

whether or not drag is enabled

interactionSource: MutableInteractionSource? = null

MutableInteractionSource that will be used to emit DragInteraction.Start when this draggable is being dragged.

startDragImmediately: Boolean = false

when set to true, draggable2D will start dragging immediately and prevent other gesture detectors from reacting to "down" events (in order to block composed press-based gestures). This is intended to allow end users to "catch" an animating widget by pressing on it. It's useful to set it when value you're dragging is settling / animating.

onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = NoOpOnDragStarted

callback that will be invoked when drag is about to start at the starting position, allowing user to suspend and perform preparation for drag, if desired.This suspend function is invoked with the draggable2D scope, allowing for async processing, if desired. Note that the scope used here is the onw provided by the draggable2D node, for long running work that needs to outlast the modifier being in the composition you should use a scope that fits the lifecycle needed.

onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit = NoOpOnDragStopped

callback that will be invoked when drag is finished, allowing the user to react on velocity and process it. This suspend function is invoked with the draggable2D scope, allowing for async processing, if desired. Note that the scope used here is the onw provided by the draggable2D scope, for long running work that needs to outlast the modifier being in the composition you should use a scope that fits the lifecycle needed.

reverseDirection: Boolean = false

reverse the direction of the scroll, so top to bottom scroll will behave like bottom to top and left to right will behave like right to left.

forEachGesture

suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit): Unit

Repeatedly calls block to handle gestures. If there is a CancellationException, it will wait until all pointers are raised before another gesture is detected, or it exits if isActive is false.

awaitEachGesture does the same thing without the possibility of missing events between gestures, but also lacks the ability to call arbitrary suspending functions within block.

suspend fun AwaitPointerEventScope.horizontalDrag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) -> Unit
): Boolean

Reads horizontal position change events for pointerId and calls onDrag for every change in position. If pointerId is raised, a new pointer is chosen from those that are down and if none exist, the method returns. This does not wait for touch slop.

Example Usage:

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitHorizontalTouchSlopOrCancellation
import androidx.compose.foundation.gestures.horizontalDrag
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var width by remember { mutableStateOf(0f) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { width = it.width.toFloat() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .fillMaxHeight()
            .width(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    val change =
                        awaitHorizontalTouchSlopOrCancellation(down.id) { change, over ->
                            val originalX = offsetX.value
                            val newValue =
                                (originalX + over).coerceIn(0f, width - 50.dp.toPx())
                            change.consume()
                            offsetX.value = newValue
                        }
                    if (change != null) {
                        horizontalDrag(change.id) {
                            val originalX = offsetX.value
                            val newValue = (originalX + it.positionChange().x)
                                .coerceIn(0f, width - 50.dp.toPx())
                            it.consume()
                            offsetX.value = newValue
                        }
                    }
                }
            }
    )
}
suspend fun TransformableState.panBy(offset: Offset): Unit

Pan without animation by a offset Offset in pixels and suspend until it's set.

Parameters
offset: Offset

offset in pixels by which to pan

rotateBy

suspend fun TransformableState.rotateBy(degrees: Float): Unit

Rotate without animation by a degrees degrees and suspend until it's set.

Parameters
degrees: Float

degrees by which to rotate

suspend fun ScrollableState.scrollBy(value: Float): Float

Jump instantly by value pixels.

Cancels the currently running scroll, if any, and suspends until the cancellation is complete.

Parameters
value: Float

number of pixels to scroll by

Returns
Float

the amount of scroll consumed

See also
animateScrollBy

for an animated version

scrollable

fun Modifier.scrollable(
    state: ScrollableState,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    flingBehavior: FlingBehavior? = null,
    interactionSource: MutableInteractionSource? = null
): Modifier

Configure touch scrolling and flinging for the UI element in a single Orientation.

Users should update their state themselves using default ScrollableState and its consumeScrollDelta callback or by implementing ScrollableState interface manually and reflect their own state in UI when using this component.

If you don't need to have fling or nested scroll support, but want to make component simply draggable, consider using draggable.

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle

// actual composable state that we will show on UI and update in `Scrollable`
val offset = remember { mutableStateOf(0f) }
Box(
    Modifier
        .size(150.dp)
        .scrollable(
            orientation = Orientation.Vertical,
            // state for Scrollable, describes how consume scroll amount
            state = rememberScrollableState { delta ->
                // use the scroll data and indicate how much this element consumed.
                // unconsumed deltas will be propagated to nested scrollables (if present)
                offset.value = offset.value + delta // update the state
                delta // indicate that we consumed all the pixels available
            }
        )
        .background(Color.LightGray),
    contentAlignment = Alignment.Center
) {
    // Modifier.scrollable is not opinionated about its children's layouts. It will however
    // promote nested scrolling capabilities if those children also use the modifier.
    // The modifier will not change any layouts so one must handle any desired changes through
    // the delta values in the scrollable state
    Text(offset.value.roundToInt().toString(), style = TextStyle(fontSize = 32.sp))
}
Parameters
state: ScrollableState

ScrollableState state of the scrollable. Defines how scroll events will be interpreted by the user land logic and contains useful information about on-going events.

orientation: Orientation

orientation of the scrolling

enabled: Boolean = true

whether or not scrolling in enabled

reverseDirection: Boolean = false

reverse the direction of the scroll, so top to bottom scroll will behave like bottom to top and left to right will behave like right to left.

flingBehavior: FlingBehavior? = null

logic describing fling behavior when drag has finished with velocity. If null, default from ScrollableDefaults.flingBehavior will be used.

interactionSource: MutableInteractionSource? = null

MutableInteractionSource that will be used to emit drag events when this scrollable is being dragged.

scrollable

@ExperimentalFoundationApi
fun Modifier.scrollable(
    state: ScrollableState,
    orientation: Orientation,
    overscrollEffect: OverscrollEffect?,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    flingBehavior: FlingBehavior? = null,
    interactionSource: MutableInteractionSource? = null,
    bringIntoViewSpec: BringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()
): Modifier

Configure touch scrolling and flinging for the UI element in a single Orientation.

Users should update their state themselves using default ScrollableState and its consumeScrollDelta callback or by implementing ScrollableState interface manually and reflect their own state in UI when using this component.

If you don't need to have fling or nested scroll support, but want to make component simply draggable, consider using draggable.

This overload provides the access to OverscrollEffect that defines the behaviour of the over scrolling logic. Consider using ScrollableDefaults.overscrollEffect for the platform look-and-feel.

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.rememberScrollableState
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle

// actual composable state that we will show on UI and update in `Scrollable`
val offset = remember { mutableStateOf(0f) }
Box(
    Modifier
        .size(150.dp)
        .scrollable(
            orientation = Orientation.Vertical,
            // state for Scrollable, describes how consume scroll amount
            state = rememberScrollableState { delta ->
                // use the scroll data and indicate how much this element consumed.
                // unconsumed deltas will be propagated to nested scrollables (if present)
                offset.value = offset.value + delta // update the state
                delta // indicate that we consumed all the pixels available
            }
        )
        .background(Color.LightGray),
    contentAlignment = Alignment.Center
) {
    // Modifier.scrollable is not opinionated about its children's layouts. It will however
    // promote nested scrolling capabilities if those children also use the modifier.
    // The modifier will not change any layouts so one must handle any desired changes through
    // the delta values in the scrollable state
    Text(offset.value.roundToInt().toString(), style = TextStyle(fontSize = 32.sp))
}
Parameters
state: ScrollableState

ScrollableState state of the scrollable. Defines how scroll events will be interpreted by the user land logic and contains useful information about on-going events.

orientation: Orientation

orientation of the scrolling

overscrollEffect: OverscrollEffect?

effect to which the deltas will be fed when the scrollable have some scrolling delta left. Pass null for no overscroll. If you pass an effect you should also apply androidx.compose.foundation.overscroll modifier.

enabled: Boolean = true

whether or not scrolling in enabled

reverseDirection: Boolean = false

reverse the direction of the scroll, so top to bottom scroll will behave like bottom to top and left to right will behave like right to left.

flingBehavior: FlingBehavior? = null

logic describing fling behavior when drag has finished with velocity. If null, default from ScrollableDefaults.flingBehavior will be used.

interactionSource: MutableInteractionSource? = null

MutableInteractionSource that will be used to emit drag events when this scrollable is being dragged.

bringIntoViewSpec: BringIntoViewSpec = ScrollableDefaults.bringIntoViewSpec()

The configuration that this scrollable should use to perform scrolling when scroll requests are received from the focus system.

Note: This API is experimental as it brings support for some experimental features: overscrollEffect and bringIntoViewScroller.

@ExperimentalFoundationApi
suspend fun <T : Any?> AnchoredDraggableState<T>.snapTo(targetValue: T): Unit

Snap to a targetValue without any animation. If the targetValue is not in the set of anchors, the AnchoredDraggableState.currentValue will be updated to the targetValue without updating the offset.

Parameters
targetValue: T

The target value of the animation

Throws
kotlinx.coroutines.CancellationException

if the interaction interrupted by another interaction like a gesture interaction or another programmatic interaction like a animateTo or snapTo call.

stopScroll

suspend fun ScrollableState.stopScroll(
    scrollPriority: MutatePriority = MutatePriority.Default
): Unit

Stop and suspend until any ongoing animation, smooth scrolling, fling, or any other scroll occurring via ScrollableState.scroll is terminated.

Parameters
scrollPriority: MutatePriority = MutatePriority.Default

scrolls that run with this priority or lower will be stopped

stopTransformation

suspend fun TransformableState.stopTransformation(
    terminationPriority: MutatePriority = MutatePriority.Default
): Unit

Stop and suspend until any ongoing TransformableState.transform with priority terminationPriority or lower is terminated.

Parameters
terminationPriority: MutatePriority = MutatePriority.Default

transformation that runs with this priority or lower will be stopped

transformable

fun Modifier.transformable(
    state: TransformableState,
    lockRotationOnZoomPan: Boolean = false,
    enabled: Boolean = true
): Modifier

Enable transformation gestures of the modified UI element.

Users should update their state themselves using default TransformableState and its onTransformation callback or by implementing TransformableState interface manually and reflect their own state in UI when using this component.

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.animateZoomBy
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset

Box(
    Modifier
        .size(200.dp)
        .clipToBounds()
        .background(Color.LightGray)
) {
    // 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 coroutineScope = rememberCoroutineScope()
    // let's create a modifier state to specify how to update our UI state defined above
    val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
        // note: scale goes by factor, not an absolute difference, so we need to multiply it
        // for this example, we don't allow downscaling, so cap it to 1f
        scale = max(scale * zoomChange, 1f)
        rotation += rotationChange
        offset += offsetChange
    }
    Box(
        Modifier
            // apply pan offset state as a layout transformation before other modifiers
            .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
            // add transformable to listen to multitouch transformation events after offset
            .transformable(state = state)
            // optional for example: add double click to zoom
            .pointerInput(Unit) {
                detectTapGestures(
                    onDoubleTap = {
                        coroutineScope.launch { state.animateZoomBy(4f) }
                    }
                )
            }
            .fillMaxSize()
            .border(1.dp, Color.Green),
        contentAlignment = Alignment.Center
    ) {
        Text(
            "\uD83C\uDF55",
            fontSize = 32.sp,
            // apply other transformations like rotation and zoom on the pizza slice emoji
            modifier = Modifier.graphicsLayer {
                scaleX = scale
                scaleY = scale
                rotationZ = rotation
            }
        )
    }
}
Parameters
state: TransformableState

TransformableState of the transformable. Defines how transformation events will be interpreted by the user land logic, contains useful information about on-going events and provides animation capabilities.

lockRotationOnZoomPan: Boolean = false

If true, rotation is allowed only if touch slop is detected for rotation before pan or zoom motions. If not, pan and zoom gestures will be detected, but rotation gestures will not be. If false, once touch slop is reached, all three gestures are detected.

enabled: Boolean = true

whether zooming by gestures is enabled or not

transformable

@ExperimentalFoundationApi
fun Modifier.transformable(
    state: TransformableState,
    canPan: (Offset) -> Boolean,
    lockRotationOnZoomPan: Boolean = false,
    enabled: Boolean = true
): Modifier

Enable transformation gestures of the modified UI element.

Users should update their state themselves using default TransformableState and its onTransformation callback or by implementing TransformableState interface manually and reflect their own state in UI when using this component.

This overload of transformable modifier provides canPan parameter, which allows the caller to control when the pan can start. making pan gesture to not to start when the scale is 1f makes transformable modifiers to work well within the scrollable container. See example:

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.animateZoomBy
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.rememberTransformableState
import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset

Row(
    Modifier
        .size(width = 120.dp, height = 100.dp)
        .horizontalScroll(rememberScrollState())
) {
    // first child of the scrollable row is a transformable
    Box(
        Modifier
            .size(100.dp)
            .clipToBounds()
            .background(Color.LightGray)
    ) {
        // 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 coroutineScope = rememberCoroutineScope()
        // let's create a modifier state to specify how to update our UI state defined above
        val state = rememberTransformableState { zoomChange, offsetChange, rotationChange ->
            // note: scale goes by factor, not an absolute difference, so we need to multiply it
            // for this example, we don't allow downscaling, so cap it to 1f
            scale = max(scale * zoomChange, 1f)
            rotation += rotationChange
            offset += offsetChange
        }
        Box(
            Modifier
                // apply pan offset state as a layout transformation before other modifiers
                .offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
                // add transformable to listen to multitouch transformation events after offset
                // To make sure our transformable work well within pager or scrolling lists,
                // disallow panning if we are not zoomed in.
                .transformable(state = state, canPan = { scale != 1f })
                // optional for example: add double click to zoom
                .pointerInput(Unit) {
                    detectTapGestures(
                        onDoubleTap = {
                            coroutineScope.launch { state.animateZoomBy(4f) }
                        }
                    )
                }
                .fillMaxSize()
                .border(1.dp, Color.Green),
            contentAlignment = Alignment.Center
        ) {
            Text(
                "\uD83C\uDF55",
                fontSize = 32.sp,
                // apply other transformations like rotation and zoom on the pizza slice emoji
                modifier = Modifier.graphicsLayer {
                    scaleX = scale
                    scaleY = scale
                    rotationZ = rotation
                }
            )
        }
    }
    // other children are just colored boxes
    Box(
        Modifier
            .size(100.dp)
            .background(Color.Red)
            .border(2.dp, Color.Black)
    )
}
Parameters
state: TransformableState

TransformableState of the transformable. Defines how transformation events will be interpreted by the user land logic, contains useful information about on-going events and provides animation capabilities.

canPan: (Offset) -> Boolean

whether the pan gesture can be performed or not given the pan offset

lockRotationOnZoomPan: Boolean = false

If true, rotation is allowed only if touch slop is detected for rotation before pan or zoom motions. If not, pan and zoom gestures will be detected, but rotation gestures will not be. If false, once touch slop is reached, all three gestures are detected.

enabled: Boolean = true

whether zooming by gestures is enabled or not

suspend fun AwaitPointerEventScope.verticalDrag(
    pointerId: PointerId,
    onDrag: (PointerInputChange) -> Unit
): Boolean

Reads vertical position change events for pointerId and calls onDrag for every change in position. If pointerId is raised, a new pointer is chosen from those that are down and if none exist, the method returns. This does not wait for touch slop

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation
import androidx.compose.foundation.gestures.verticalDrag
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntOffset

val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
var height by remember { mutableStateOf(0f) }
Box(
    Modifier.fillMaxSize()
        .onSizeChanged { height = it.height.toFloat() }
) {
    Box(
        Modifier.offset { IntOffset(offsetX.value.roundToInt(), offsetY.value.roundToInt()) }
            .fillMaxWidth()
            .height(50.dp)
            .background(Color.Blue)
            .pointerInput(Unit) {
                awaitEachGesture {
                    val down = awaitFirstDown()
                    val change =
                        awaitVerticalTouchSlopOrCancellation(down.id) { change, over ->
                            val originalY = offsetY.value
                            val newValue = (originalY + over)
                                .coerceIn(0f, height - 50.dp.toPx())
                            change.consume()
                            offsetY.value = newValue
                        }
                    if (change != null) {
                        verticalDrag(change.id) {
                            val originalY = offsetY.value
                            val newValue = (originalY + it.positionChange().y)
                                .coerceIn(0f, height - 50.dp.toPx())
                            it.consume()
                            offsetY.value = newValue
                        }
                    }
                }
            }
    )
}
Returns
Boolean

true if the vertical drag completed normally or false if the drag motion was canceled by another gesture detector consuming position change events.

Example Usage:

waitForUpOrCancellation

suspend fun AwaitPointerEventScope.waitForUpOrCancellation(
    pass: PointerEventPass = PointerEventPass.Main
): PointerInputChange?

Reads events in the given pass until all pointers are up or the gesture was canceled. The gesture is considered canceled when a pointer leaves the event region, a position change has been consumed or a pointer down change event was already consumed in the given pass. If the gesture was not canceled, the final up change is returned or null if the event was canceled.

suspend fun TransformableState.zoomBy(zoomFactor: Float): Unit

Zoom without animation by a ratio of zoomFactor over the current size and suspend until it's set.

Parameters
zoomFactor: Float

ratio over the current size by which to zoom