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:
-
Drag APIs such as
drag,draggable,detectDragGestures,horizontalDrag, andrememberDraggableState. -
Scroll APIs such as
animateScrollBy,scrollable, andrememberScrollableState. -
And other gestures APIs such as
animateZoomBy,animateRotateBy,detectTapGestures, andawaitVerticalDragOrCancellation.
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 |
Cmn
|
DraggableState |
State of |
Cmn
|
FlingBehavior |
Interface to specify fling behavior. |
Cmn
|
PressGestureScope |
Receiver scope for |
Cmn
|
Scroll2DScope |
Scope used for suspending scroll blocks |
Cmn
|
ScrollScope |
Scope used for suspending scroll blocks |
Cmn
|
Scrollable2DState |
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 |
Cmn
|
Classes
AnchoredDraggableState |
State of the |
Cmn
|
DraggableAnchorsConfig |
|
Cmn
|
Exceptions
GestureCancellationException |
A gesture was canceled and cannot continue, likely because another gesture has taken over the pointer input stream. |
Cmn
|
Objects
AnchoredDraggableDefaults |
Contains useful defaults for use with |
Cmn
|
ScrollableDefaults |
Contains the default values used by |
Cmn
|
Annotations
ExperimentalTapGestureDetectorBehaviorApi |
This annotation is deprecated. The flag for this opt-in marker has been moved to ComposeFoundationFlags and renamed to isDetectTapGesturesImmediateCoroutineDispatchEnabled. |
Cmn
|
Enums
Orientation |
Class to define possible directions in which common gesture modifiers like |
Cmn
|
Top-level functions summary
AnchoredDraggableState<T> |
<T : Any?> This function is deprecated. This constructor of AnchoredDraggableState has been deprecated. |
Cmn
|
AnchoredDraggableState<T> |
<T : Any?> This function is deprecated. This constructor of AnchoredDraggableState has been deprecated. |
Cmn
|
Draggable2DState |
Draggable2DState(onDelta: (Offset) -> Unit)Default implementation of |
Cmn
|
DraggableAnchors<T> |
<T : Any> DraggableAnchors(builder: DraggableAnchorsConfig<T>.() -> Unit)Create a new |
Cmn
|
DraggableState |
DraggableState(onDelta: (Float) -> Unit)Default implementation of |
Cmn
|
Scrollable2DState |
Scrollable2DState(consumeScrollDelta: (Offset) -> Offset)Default implementation of |
Cmn
|
ScrollableState |
ScrollableState(consumeScrollDelta: (Float) -> Float)Default implementation of |
Cmn
|
TransformableState |
TransformableState(Default implementation of |
Cmn
|
Draggable2DState |
@ComposableCreate and remember default implementation of |
Cmn
|
DraggableState |
@ComposableCreate and remember default implementation of |
Cmn
|
Scrollable2DState |
@ComposableCreate and remember the default implementation of |
Cmn
|
ScrollableState |
@ComposableCreate and remember the default implementation of |
Cmn
|
TransformableState |
@ComposableCreate and remember default implementation of |
Cmn
|
Extension functions summary
Modifier |
<T : Any?> Modifier.anchoredDraggable(Enable drag gestures between a set of predefined values. |
Cmn
|
Modifier |
<T : Any?> Modifier.This function is deprecated. startDragImmediately has been removed without replacement. |
Cmn
|
Modifier |
<T : Any?> Modifier.anchoredDraggable(Enable drag gestures between a set of predefined values. |
Cmn
|
Modifier |
<T : Any?> Modifier.This function is deprecated. startDragImmediately has been removed without replacement. |
Cmn
|
suspend Unit |
TransformableState.animateBy(Animate zoom, pan, and rotation simultaneously and suspend until the animation is finished. |
Cmn
|
suspend Unit |
TransformableState.animatePanBy(Animate pan by |
Cmn
|
suspend Unit |
TransformableState.animateRotateBy(Animate rotate by a ratio of |
Cmn
|
suspend Float |
ScrollableState.animateScrollBy(Scroll by |
Cmn
|
suspend Offset |
Scrollable2DState.animateScrollBy(Scroll by |
Cmn
|
suspend Unit |
<T : Any?> AnchoredDraggableState<T>.animateTo(Animate to a |
Cmn
|
suspend Float |
<T : Any?> AnchoredDraggableState<T>.animateToWithDecay(Attempt to animate using decay Animation to a |
Cmn
|
suspend Unit |
TransformableState.animateZoomBy(Animate zoom by a ratio of |
Cmn
|
suspend PointerInputChange? |
AwaitPointerEventScope.awaitDragOrCancellation(pointerId: PointerId)Reads pointer input events until a drag is detected or all pointers are up. |
Cmn
|
suspend Unit |
PointerInputScope.awaitEachGesture(block: suspend AwaitPointerEventScope.() -> Unit)Repeatedly calls |
Cmn
|
suspend PointerInputChange |
AwaitPointerEventScope.awaitFirstDown(Reads events until the first down is received in the given |
Cmn
|
suspend PointerInputChange? |
Reads pointer input events until a horizontal drag is detected or all pointers are up. |
Cmn
|
suspend PointerInputChange? |
AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(Waits for horizontal drag motion to pass |
Cmn
|
suspend PointerInputChange? |
AwaitPointerEventScope.awaitHorizontalTouchSlopOrCancellation(Waits for horizontal drag motion to pass |
Cmn
|
suspend PointerInputChange? |
AwaitPointerEventScope.awaitLongPressOrCancellation(Waits for a long press by examining |
Cmn
|
suspend PointerInputChange? |
AwaitPointerEventScope.awaitTouchSlopOrCancellation(Waits for drag motion to pass |
Cmn
|
suspend PointerInputChange? |
Reads pointer input events until a vertical drag is detected or all pointers are up. |
Cmn
|
suspend PointerInputChange? |
AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(Waits for vertical drag motion to pass |
Cmn
|
suspend PointerInputChange? |
AwaitPointerEventScope.awaitVerticalTouchSlopOrCancellation(Waits for vertical drag motion to pass |
Cmn
|
Offset |
PointerEvent.calculateCentroid(useCurrent: Boolean)Returns the centroid of all pointers that are down and were previously down. |
Cmn
|
Float |
PointerEvent.calculateCentroidSize(useCurrent: Boolean)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 |
Cmn
|
Float |
Uses the change of the centroid size between the |
Cmn
|
suspend Unit |
PointerInputScope.detectDragGestures(Gesture detector that waits for pointer down and touch slop in any direction and then calls |
Cmn
|
suspend Unit |
PointerInputScope.detectDragGestures(A Gesture detector that waits for pointer down and touch slop in the direction specified by |
Cmn
|
suspend Unit |
PointerInputScope.detectDragGesturesAfterLongPress(Gesture detector that waits for pointer down and long press, after which it calls |
Cmn
|
suspend Unit |
PointerInputScope.detectHorizontalDragGestures(Gesture detector that waits for pointer down and touch slop in the horizontal direction and then calls |
Cmn
|
suspend Unit |
PointerInputScope.detectTapGestures(Detects tap, double-tap, and long press gestures and calls |
Cmn
|
suspend Unit |
PointerInputScope.detectTransformGestures(A gesture detector for rotation, panning, and zoom. |
Cmn
|
suspend Unit |
PointerInputScope.detectVerticalDragGestures(Gesture detector that waits for pointer down and touch slop in the vertical direction and then calls |
Cmn
|
suspend Boolean |
AwaitPointerEventScope.drag(Reads position change events for |
Cmn
|
Modifier |
Modifier.draggable(Configure touch dragging for the UI element in a single |
Cmn
|
Modifier |
Modifier.draggable2D(Configure touch dragging for the UI element in both orientations. |
Cmn
|
inline Unit |
<T : Any?> DraggableAnchors<T>.forEach(block: (key, position: Float) -> Unit)Iterate over all the anchors. |
Cmn
|
suspend Unit |
PointerInputScope.This function is deprecated. Use awaitEachGesture instead. forEachGesture() can drop events between gestures. |
Cmn
|
suspend Boolean |
AwaitPointerEventScope.horizontalDrag(Reads horizontal position change events for |
Cmn
|
suspend Unit |
TransformableState.panBy(offset: Offset)Pan without animation by a |
Cmn
|
suspend Unit |
TransformableState.rotateBy(degrees: Float)Rotate without animation by a |
Cmn
|
suspend Float |
ScrollableState.scrollBy(value: Float)Jump instantly by |
Cmn
|
suspend Offset |
Scrollable2DState.scrollBy(value: Offset)Jump instantly by |
Cmn
|
Modifier |
Modifier.scrollable(Configure touch scrolling and flinging for the UI element in a single |
Cmn
|
Modifier |
Modifier.scrollable(Configure touch scrolling and flinging for the UI element in a single |
Cmn
|
Modifier |
Modifier.scrollable2D(Configure touch scrolling and flinging for the UI element in both XY orientations. |
Cmn
|
suspend Unit |
<T : Any?> AnchoredDraggableState<T>.snapTo(targetValue: T)Snap to a |
Cmn
|
suspend Unit |
ScrollableState.stopScroll(scrollPriority: MutatePriority)Stop and suspend until any ongoing animation, smooth scrolling, fling, or any other scroll occurring via |
Cmn
|
suspend Unit |
Scrollable2DState.stopScroll(scrollPriority: MutatePriority)Stop and suspend until any ongoing animation, smooth scrolling, fling, or any other scroll occurring via |
Cmn
|
suspend Unit |
TransformableState.stopTransformation(Stop and suspend until any ongoing |
Cmn
|
Modifier |
Modifier.transformable(Enable transformation gestures of the modified UI element. |
Cmn
|
Modifier |
Modifier.transformable(Enable transformation gestures of the modified UI element. |
Cmn
|
suspend Boolean |
AwaitPointerEventScope.verticalDrag(Reads vertical position change events for |
Cmn
|
suspend PointerInputChange? |
Reads events in the given |
Cmn
|
suspend Unit |
TransformableState.zoomBy(zoomFactor: Float)Zoom without animation by a ratio of |
Cmn
|
Top-level properties summary
Boolean |
This property is deprecated. This flag has been moved to ComposeFoundationFlags and renamed to isDetectTapGesturesImmediateCoroutineDispatchEnabled. |
Cmn
|
ProvidableCompositionLocal<BringIntoViewSpec> |
A composition local to customize the focus scrolling behavior used by some scrollable containers. |
Cmn
android
|
Top-level functions
AnchoredDraggableState
fun <T : Any?>AnchoredDraggableState(
initialValue: T,
positionalThreshold: (totalDistance: Float) -> Float,
velocityThreshold: () -> Float,
snapAnimationSpec: AnimationSpec<Float>,
decayAnimationSpec: DecayAnimationSpec<Float>,
confirmValueChange: (newValue) -> Boolean = { true }
): AnchoredDraggableState<T>
State of the anchoredDraggable modifier. Use the constructor overload with anchors if the anchors are defined in composition, or update the anchors using AnchoredDraggableState.updateAnchors.
This contains necessary information about any ongoing drag or animation and provides methods to change the state either immediately or by starting an animation.
| Parameters | |
|---|---|
initialValue: T |
The initial value of the state. |
positionalThreshold: (totalDistance: Float) -> Float |
The positional threshold, in px, to be used when calculating the target state while a drag is in progress and when settling after the drag ends. This is the distance from the start of a transition. It will be, depending on the direction of the interaction, added or subtracted from/to the origin offset. It should always be a positive value. |
velocityThreshold: () -> Float |
The velocity threshold (in px per second) that the end velocity has to exceed in order to animate to the next state, even if the |
confirmValueChange: (newValue) -> Boolean = { true } |
Optional callback invoked to confirm or veto a pending state change. |
AnchoredDraggableState
fun <T : Any?>AnchoredDraggableState(
initialValue: T,
anchors: DraggableAnchors<T>,
positionalThreshold: (totalDistance: Float) -> Float,
velocityThreshold: () -> Float,
snapAnimationSpec: AnimationSpec<Float>,
decayAnimationSpec: DecayAnimationSpec<Float>,
confirmValueChange: (newValue) -> Boolean = { true }
): AnchoredDraggableState<T>
Construct an AnchoredDraggableState instance with anchors.
| Parameters | |
|---|---|
initialValue: T |
The initial value of the state. |
anchors: DraggableAnchors<T> |
The anchors of the state. Use |
positionalThreshold: (totalDistance: Float) -> Float |
The positional threshold, in px, to be used when calculating the target state while a drag is in progress and when settling after the drag ends. This is the distance from the start of a transition. It will be, depending on the direction of the interaction, added or subtracted from/to the origin offset. It should always be a positive value. |
velocityThreshold: () -> Float |
The velocity threshold (in px per second) that the end velocity has to exceed in order to animate to the next state, even if the |
snapAnimationSpec: AnimationSpec<Float> |
The default animation spec that will be used to animate to a new state. |
decayAnimationSpec: DecayAnimationSpec<Float> |
The animation spec that will be used when flinging with a large enough velocity to reach or cross the target state. |
confirmValueChange: (newValue) -> Boolean = { true } |
Optional callback invoked to confirm or veto a pending state change. |
Draggable2DState
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.
DraggableAnchors
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 |
| Returns | |
|---|---|
DraggableAnchors<T> |
A new |
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.
Scrollable2DState
fun Scrollable2DState(consumeScrollDelta: (Offset) -> Offset): Scrollable2DState
Default implementation of Scrollable2DState interface that contains necessary information about the ongoing fling and provides smooth scrolling capabilities.
This is the simplest way to set up a scrollable2D modifier. When constructing this Scrollable2DState, 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.
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.
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, |
rememberDraggable2DState
@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 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.
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.
rememberScrollable2DState
@Composable
fun rememberScrollable2DState(consumeScrollDelta: (Offset) -> Offset): Scrollable2DState
Create and remember the default implementation of Scrollable2DState interface that contains necessary information about the ongoing fling and provides smooth scrolling capabilities.
This is the simplest way to set up a scrollable2D modifier. When constructing this Scrollable2DState, 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.
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.
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, |
Extension functions
anchoredDraggable
fun <T : Any?> Modifier.anchoredDraggable(
state: AnchoredDraggableState<T>,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
overscrollEffect: OverscrollEffect? = null,
flingBehavior: FlingBehavior? = null
): 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. If the orientation is set to Orientation.Horizontal and LocalLayoutDirection's value is LayoutDirection.Rtl, the drag deltas will be reversed. 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 |
orientation: Orientation |
The orientation in which the |
enabled: Boolean = true |
Whether this |
interactionSource: MutableInteractionSource? = null |
Optional |
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 |
flingBehavior: FlingBehavior? = null |
Optionally configure how the anchored draggable performs the fling. By default (if passing in null), this will snap to the closest anchor considering the velocity thresholds and positional thresholds. See |
anchoredDraggable
fun <T : Any?> Modifier.anchoredDraggable(
state: AnchoredDraggableState<T>,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
overscrollEffect: OverscrollEffect? = null,
startDragImmediately: Boolean = state.isAnimationRunning,
flingBehavior: FlingBehavior? = null
): 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. If the orientation is set to Orientation.Horizontal and LocalLayoutDirection's value is LayoutDirection.Rtl, the drag deltas will be reversed. 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 |
orientation: Orientation |
The orientation in which the |
enabled: Boolean = true |
Whether this |
interactionSource: MutableInteractionSource? = null |
Optional |
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 |
startDragImmediately: Boolean = state.isAnimationRunning |
when set to false, |
flingBehavior: FlingBehavior? = null |
Optionally configure how the anchored draggable performs the fling. By default (if passing in null), this will snap to the closest anchor considering the velocity thresholds and positional thresholds. See |
anchoredDraggable
fun <T : Any?> Modifier.anchoredDraggable(
state: AnchoredDraggableState<T>,
reverseDirection: Boolean,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
overscrollEffect: OverscrollEffect? = null,
flingBehavior: FlingBehavior? = null
): 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 |
reverseDirection: Boolean |
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. If not specified, this will be determined based on |
orientation: Orientation |
The orientation in which the |
enabled: Boolean = true |
Whether this |
interactionSource: MutableInteractionSource? = null |
Optional |
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 |
flingBehavior: FlingBehavior? = null |
Optionally configure how the anchored draggable performs the fling. By default (if passing in null), this will snap to the closest anchor considering the velocity thresholds and positional thresholds. See |
anchoredDraggable
fun <T : Any?> Modifier.anchoredDraggable(
state: AnchoredDraggableState<T>,
reverseDirection: Boolean,
orientation: Orientation,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
overscrollEffect: OverscrollEffect? = null,
startDragImmediately: Boolean = state.isAnimationRunning,
flingBehavior: FlingBehavior? = null
): 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 |
reverseDirection: Boolean |
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. If not specified, this will be determined based on |
orientation: Orientation |
The orientation in which the |
enabled: Boolean = true |
Whether this |
interactionSource: MutableInteractionSource? = null |
Optional |
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 |
startDragImmediately: Boolean = state.isAnimationRunning |
when set to false, |
flingBehavior: FlingBehavior? = null |
Optionally configure how the anchored draggable performs the fling. By default (if passing in null), this will snap to the closest anchor considering the velocity thresholds and positional thresholds. See |
animateBy
suspend fun TransformableState.animateBy(
zoomFactor: Float,
panOffset: Offset,
rotationDegrees: Float,
zoomAnimationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow),
panAnimationSpec: AnimationSpec<Offset> = SpringSpec(stiffness = Spring.StiffnessLow),
rotationAnimationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow)
): Unit
Animate zoom, pan, and rotation simultaneously and suspend until the animation is finished.
Zoom is animated by a ratio of zoomFactor over the current size. Pan is animated by panOffset in pixels. Rotation is animated by the value of rotationDegrees clockwise. Any of these parameters can be set to a no-op value that will result in no animation of that parameter. The no-op values are the following: 1f for zoomFactor, Offset.Zero for panOffset, and 0f for rotationDegrees.
import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.animateBy 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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) // detect tap gestures: // 1) single tap to simultaneously animate zoom, pan, and rotation // 2) double tap to animate back to the initial position .pointerInput(Unit) { detectTapGestures( onTap = { coroutineScope.launch { state.animateBy( zoomFactor = 1.5f, panOffset = Offset(20f, 20f), rotationDegrees = 90f, zoomAnimationSpec = spring(), panAnimationSpec = tween(durationMillis = 1000), rotationAnimationSpec = spring(), ) } }, onDoubleTap = { coroutineScope.launch { state.animateBy(1 / scale, -offset, -rotation) } }, ) } .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 | |
|---|---|
zoomFactor: Float |
ratio over the current size by which to zoom. For example, if |
panOffset: Offset |
offset to pan, in pixels |
rotationDegrees: Float |
the degrees by which to rotate clockwise |
zoomAnimationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow) |
|
panAnimationSpec: AnimationSpec<Offset> = SpringSpec(stiffness = Spring.StiffnessLow) |
|
rotationAnimationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow) |
|
animatePanBy
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) |
|
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 |
the degrees by which to rotate clockwise |
animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow) |
|
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() |
|
| Returns | |
|---|---|
Float |
the amount of scroll consumed |
animateScrollBy
suspend fun Scrollable2DState.animateScrollBy(
value: Offset,
animationSpec: AnimationSpec<Offset> = spring()
): Offset
Scroll by value pixels with animation.
Cancels the currently running scroll, if any, and suspends until the cancellation is complete.
| Parameters | |
|---|---|
value: Offset |
number of pixels to scroll by |
animationSpec: AnimationSpec<Offset> = spring() |
|
| Returns | |
|---|---|
Offset |
the amount of scroll consumed |
animateTo
suspend fun <T : Any?> AnchoredDraggableState<T>.animateTo(
targetValue: T,
animationSpec: AnimationSpec<Float> = if (usePreModifierChangeBehavior) { @Suppress("DEPRECATION") this.snapAnimationSpec } else AnchoredDraggableDefaults.SnapAnimationSpec
): 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 |
animationSpec: AnimationSpec<Float> = if (usePreModifierChangeBehavior) {
@Suppress("DEPRECATION") this.snapAnimationSpec
} else AnchoredDraggableDefaults.SnapAnimationSpec |
The animation spec used to perform the animation |
| Throws | |
|---|---|
kotlinx.coroutines.CancellationException |
if the interaction interrupted by another interaction like a gesture interaction or another programmatic interaction like a |
animateToWithDecay
suspend fun <T : Any?> AnchoredDraggableState<T>.animateToWithDecay(
targetValue: T,
velocity: Float,
snapAnimationSpec: AnimationSpec<Float> = if (usePreModifierChangeBehavior) { @Suppress("DEPRECATION") this.snapAnimationSpec } else AnchoredDraggableDefaults.SnapAnimationSpec,
decayAnimationSpec: DecayAnimationSpec<Float> = if (usePreModifierChangeBehavior) { @Suppress("DEPRECATION") this.decayAnimationSpec } else AnchoredDraggableDefaults.DecayAnimationSpec
): 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 decayAnimationSpec to get to that offset and return the consumed velocity. If the velocity is not high enough, we'll use 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, in px/s |
snapAnimationSpec: AnimationSpec<Float> = if (usePreModifierChangeBehavior) {
@Suppress("DEPRECATION") this.snapAnimationSpec
} else AnchoredDraggableDefaults.SnapAnimationSpec |
The animation spec used if the velocity is not high enough to perform a decay to the |
decayAnimationSpec: DecayAnimationSpec<Float> = if (usePreModifierChangeBehavior) {
@Suppress("DEPRECATION") this.decayAnimationSpec
} else AnchoredDraggableDefaults.DecayAnimationSpec |
The animation spec used if the velocity is high enough to perform a decay to the |
| 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 |
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 |
animationSpec: AnimationSpec<Float> = SpringSpec(stiffness = Spring.StiffnessLow) |
|
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.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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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.
awaitFirstDown
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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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 } } } } ) }
awaitHorizontalPointerSlopOrCancellation
suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
pointerId: PointerId,
pointerType: PointerType,
onPointerSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
): PointerInputChange?
Waits for horizontal drag motion to pass pointerType's 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 awaitHorizontalPointerSlopOrCancellation is called, then null is returned.
onPointerSlopReached is called after pointerType's touch slop motion in the horizontal direction with the change that caused the motion beyond touch slop and the pixels beyond touch slop. onPointerSlopReached 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.
| Returns | |
|---|---|
PointerInputChange? |
The |
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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp 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 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.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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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 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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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 } } } } ) }
awaitVerticalPointerSlopOrCancellation
suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
pointerId: PointerId,
pointerType: PointerType,
onPointerSlopReached: (change: PointerInputChange, overSlop: Float) -> Unit
): PointerInputChange?
Waits for vertical drag motion to pass pointerType's 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 awaitVerticalPointerSlopOrCancellation is called, then null is returned.
onPointerSlopReached 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. onPointerSlopReached 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.
| Returns | |
|---|---|
PointerInputChange? |
The |
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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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 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.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color 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.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color 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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color 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.Modifier import androidx.compose.ui.graphics.Color 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.Modifier import androidx.compose.ui.graphics.Color 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. @see detectDragGestures with orientation lock for a fuller set of capabilities.
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.gestures.drag import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize 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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp 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 } } ) }
| See also | |
|---|---|
detectVerticalDragGestures |
|
detectHorizontalDragGestures |
|
detectDragGesturesAfterLongPress |
to detect gestures after long press |
detectDragGestures
suspend fun PointerInputScope.detectDragGestures(
orientationLock: Orientation?,
onDragStart: (down: PointerInputChange, slopTriggerChange: PointerInputChange, overSlopOffset: Offset) -> Unit = { _, _, _ -> },
onDragEnd: (change: PointerInputChange) -> Unit = {},
onDragCancel: () -> Unit = {},
shouldAwaitTouchSlop: () -> Boolean = { true },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
): Unit
A Gesture detector that waits for pointer down and touch slop in the direction specified by orientationLock 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, the amount of drag over the touch slop is reported as the first drag event onDrag after the slop is crossed. If shouldAwaitTouchSlop returns true the touch slop recognition phase will be ignored and the drag gesture will be recognized immediately.The first onDrag in this case will report an Offset.Zero.
onDragStart is called when the touch slop has been passed and includes an Offset representing the last known pointer position relative to the containing element as well as the initial down event that triggered this gesture detection cycle. 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 with the event change of the up event and onDragCancel is called if another gesture has consumed pointer input, canceling this gesture.
import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize 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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp 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 } } ) }
| Parameters | |
|---|---|
orientationLock: Orientation? |
Optionally locks detection to this orientation, this means, when this is provided, touch slop detection and drag event detection will be conditioned to the given orientation axis. |
onDragStart: (down: PointerInputChange, slopTriggerChange: PointerInputChange, overSlopOffset: Offset) -> Unit = { _, _, _ ->
} |
A lambda to be called when the drag gesture starts, it contains information about the last known |
onDragEnd: (change: PointerInputChange) -> Unit = {} |
A lambda to be called when the gesture ends. It contains information about the up |
onDragCancel: () -> Unit = {} |
A lambda to be called when the gesture is cancelled either by an error or when it was consumed. |
shouldAwaitTouchSlop: () -> Boolean = { true } |
Indicates if touch slop detection should be skipped. |
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit |
A lambda to be called for each delta event in the gesture. It contains information about the Example Usage: |
| See also | |
|---|---|
detectVerticalDragGestures |
|
detectHorizontalDragGestures |
|
detectDragGesturesAfterLongPress |
to detect gestures after long press |
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.detectDragGestures import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize 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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp 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.gestures.drag 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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp 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.foundation.layout.offset import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color 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) val cos = cos(angleInRadians) val sin = sin(angleInRadians) return Offset((x * cos - y * sin).toFloat(), (x * sin + y * cos).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.gestures.drag 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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp 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 } } ) }
drag
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.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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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 |
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.Orientation 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.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp // 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 |
|
orientation: Orientation |
orientation of the drag |
enabled: Boolean = true |
whether or not drag is enabled |
interactionSource: MutableInteractionSource? = null |
|
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
fun Modifier.draggable2D(
state: Draggable2DState,
enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null,
startDragImmediately: Boolean = false,
onDragStarted: (startedPosition: Offset) -> Unit = NoOpOnDragStart,
onDragStopped: (velocity: Velocity) -> Unit = NoOpOnDragStop,
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.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp // 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 |
|
enabled: Boolean = true |
whether or not drag is enabled |
interactionSource: MutableInteractionSource? = null |
|
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: (startedPosition: Offset) -> Unit = NoOpOnDragStart |
callback that will be invoked when drag is about to start at the starting position, allowing user to perform preparation for drag. |
onDragStopped: (velocity: Velocity) -> Unit = NoOpOnDragStop |
callback that will be invoked when drag is finished, allowing the user to react on velocity and process it. |
reverseDirection: Boolean = false |
reverse the direction of the dragging, so top to bottom dragging will behave like bottom to top and left to right will behave like right to left. |
forEach
inline fun <T : Any?> DraggableAnchors<T>.forEach(block: (key, position: Float) -> Unit): Unit
Iterate over all the anchors.
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.
horizontalDrag
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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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 } } } } ) }
panBy
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 |
scrollBy
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 |
scrollBy
suspend fun Scrollable2DState.scrollBy(value: Offset): Offset
Jump instantly by value pixels.
Cancels the currently running scroll, if any, and suspends until the cancellation is complete.
| Parameters | |
|---|---|
value: Offset |
number of pixels to scroll by |
| Returns | |
|---|---|
Offset |
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.
scrollable is a low level modifier that handles low level scrolling input gestures, without other behaviors commonly used for scrollable containers. For building scrollable containers, see androidx.compose.foundation.scrollableArea. scrollableArea clips its content to its bounds, renders overscroll, and adjusts the direction of scroll gestures to ensure that the content moves with the user's gestures. See also androidx.compose.foundation.verticalScroll and androidx.compose.foundation.horizontalScroll for high level scrollable containers that handle layout and move the content as the user scrolls.
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.Orientation 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp // actual composable state that we will show on UI and update in `Scrollable` var offset by 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 = offset + 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.roundToInt().toString(), style = TextStyle(fontSize = 32.sp)) }
| Parameters | |
|---|---|
state: ScrollableState |
|
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 |
interactionSource: MutableInteractionSource? = null |
|
scrollable
fun Modifier.scrollable(
state: ScrollableState,
orientation: Orientation,
overscrollEffect: OverscrollEffect?,
enabled: Boolean = true,
reverseDirection: Boolean = false,
flingBehavior: FlingBehavior? = null,
interactionSource: MutableInteractionSource? = null,
bringIntoViewSpec: BringIntoViewSpec? = 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.
scrollable is a low level modifier that handles low level scrolling input gestures, without other behaviors commonly used for scrollable containers. For building scrollable containers, see androidx.compose.foundation.scrollableArea. scrollableArea clips its content to its bounds, renders overscroll, and adjusts the direction of scroll gestures to ensure that the content moves with the user's gestures. See also androidx.compose.foundation.verticalScroll and androidx.compose.foundation.horizontalScroll for high level scrollable containers that handle layout and move the content as the user scrolls.
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. Use androidx.compose.foundation.rememberOverscrollEffect to create an instance of the current provided overscroll implementation. Note: compared to other APIs that accept overscrollEffect such as scrollableArea and verticalScroll, scrollable does not render the overscroll, it only provides events. Manually add androidx.compose.foundation.overscroll to render the overscroll or use other APIs.
import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp // actual composable state that we will show on UI and update in `Scrollable` var offset by 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 = offset + 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.roundToInt().toString(), style = TextStyle(fontSize = 32.sp)) }
| Parameters | |
|---|---|
state: ScrollableState |
|
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 |
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 |
interactionSource: MutableInteractionSource? = null |
|
bringIntoViewSpec: BringIntoViewSpec? = null |
The configuration that this scrollable should use to perform scrolling when scroll requests are received from the focus system. If null is provided the system will use the behavior provided by |
scrollable2D
fun Modifier.scrollable2D(
state: Scrollable2DState,
enabled: Boolean = true,
overscrollEffect: OverscrollEffect? = null,
flingBehavior: FlingBehavior? = null,
interactionSource: MutableInteractionSource? = null
): Modifier
Configure touch scrolling and flinging for the UI element in both XY orientations.
Users should update their state themselves using default Scrollable2DState and its consumeScrollDelta callback or by implementing Scrollable2DState 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 draggable2D. If you're only interested in a single direction scroll, consider using scrollable.
This overload provides the access to OverscrollEffect that defines the behaviour of the over scrolling logic. Use androidx.compose.foundation.rememberOverscrollEffect to create an instance of the current provided overscroll implementation.
import androidx.compose.foundation.background import androidx.compose.foundation.gestures.rememberScrollable2DState import androidx.compose.foundation.gestures.scrollable2D 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp // actual composable state that we will show on UI and update in `Scrollable` val offset = remember { mutableStateOf(Offset.Zero) } Box( Modifier.size(150.dp) .scrollable2D( // state for Scrollable, describes how to consume scroll amount state = rememberScrollable2DState { 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( "X=${offset.value.x.roundToInt()} Y=${offset.value.y.roundToInt()}", style = TextStyle(fontSize = 32.sp), ) }
| Parameters | |
|---|---|
state: Scrollable2DState |
|
enabled: Boolean = true |
whether or not scrolling is enabled |
overscrollEffect: OverscrollEffect? = null |
effect to which the deltas will be fed when the scrollable have some scrolling delta left. Pass |
flingBehavior: FlingBehavior? = null |
logic describing fling behavior when drag has finished with velocity. If |
interactionSource: MutableInteractionSource? = null |
|
snapTo
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 |
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 |
stopScroll
suspend fun Scrollable2DState.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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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 |
|
lockRotationOnZoomPan: Boolean = false |
If |
enabled: Boolean = true |
whether zooming by gestures is enabled or not |
transformable
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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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 |
|
canPan: (Offset) -> Boolean |
whether the pan gesture can be performed or not given the pan offset |
lockRotationOnZoomPan: Boolean = false |
If |
enabled: Boolean = true |
whether zooming by gestures is enabled or not |
verticalDrag
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.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color 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.dp 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 |
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.
zoomBy
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 |
Top-level properties
DetectTapGesturesEnableNewDispatchingBehavior
@ExperimentalTapGestureDetectorBehaviorApi
var DetectTapGesturesEnableNewDispatchingBehavior: Boolean
Whether to use more immediate coroutine dispatching in detectTapGestures and detectTapAndPress, true by default. This might affect some implicit timing guarantees. Please file a bug if this change is affecting your use case.
LocalBringIntoViewSpec
val LocalBringIntoViewSpec: ProvidableCompositionLocal<BringIntoViewSpec>
A composition local to customize the focus scrolling behavior used by some scrollable containers. LocalBringIntoViewSpec has a platform defined default behavior.