高级动画示例:手势
使用集合让一切井井有条
根据您的偏好保存内容并对其进行分类。
与单独处理动画相比,当我们处理触摸事件和动画时,必须考虑几个事项。首先,当触摸事件开始时,我们可能需要中断正在播放的动画,因为用户互动应当具有最高优先级。
在下面的示例中,我们使用 Animatable
表示圆形组件的偏移位置。触摸事件由 pointerInput
修饰符处理。当检测到新的点按事件时,我们将调用 animateTo
以将偏移值通过动画过渡到点按位置。在动画播放期间也可能发生点按事件。在这种情况下,animateTo
会中断正在播放的动画,启动动画以过渡到新的目标位置,同时保持被中断的动画的速度。
@Composable
fun Gesture() {
val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
coroutineScope {
while (true) {
// Detect a tap event and obtain its position.
awaitPointerEventScope {
val position = awaitFirstDown().position
launch {
// Animate to the tap position.
offset.animateTo(position)
}
}
}
}
}
) {
Circle(modifier = Modifier.offset { offset.value.toIntOffset() })
}
}
private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
另一种常见模式是需要将动画值与来自触摸事件(例如拖动)的值同步。在下面的示例中,我们会看到以 Modifier
的形式(而不是使用 SwipeToDismiss
可组合项)实现的“滑动关闭”。该元素的水平偏移量表示为 Animatable
。此 API 具有可用于手势动画的特征。其值可由触摸事件和动画更改。收到触摸事件时,我们通过 stop
方法停止 Animatable
,以便拦截任何正在播放的动画。
在拖动事件期间,我们使用 snapTo
将 Animatable
值更新为从触摸事件计算得出的值。对于快速滑动,Compose 可提供 VelocityTracker
来记录拖动事件并计算速度。速度可直接馈送至投掷动画的 animateDecay
。如需将偏移值滑回原始位置,可使用 animateTo
方法指定 0f
的目标偏移值。
fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate fling decay.
val decay = splineBasedDecay<Float>(this)
// Use suspend functions for touch events and the Animatable.
coroutineScope {
while (true) {
val velocityTracker = VelocityTracker()
// Stop any ongoing animation.
offsetX.stop()
awaitPointerEventScope {
// Detect a touch down event.
val pointerId = awaitFirstDown().id
horizontalDrag(pointerId) { change ->
// Update the animation value with touch events.
launch {
offsetX.snapTo(
offsetX.value + change.positionChange().x
)
}
velocityTracker.addPosition(
change.uptimeMillis,
change.position
)
}
}
// No longer receiving touch events. Prepare the animation.
val velocity = velocityTracker.calculateVelocity().x
val targetOffsetX = decay.calculateTargetValue(
offsetX.value,
velocity
)
// The animation stops when it reaches the bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(
targetValue = 0f,
initialVelocity = velocity
)
} else {
// The element was swiped away.
offsetX.animateDecay(velocity, decay)
onDismissed()
}
}
}
}
}
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
为您推荐
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。
最后更新时间 (UTC):2025-08-24。
[null,null,["最后更新时间 (UTC):2025-08-24。"],[],[],null,["# Advanced animation example: Gestures\n\nThere are several things we have to take into consideration when we are working\nwith touch events and animations, compared to when we are working with\nanimations alone. First of all, we might need to interrupt an ongoing animation\nwhen touch events begin as user interaction should have the highest priority.\n\nIn the example below, we use an `Animatable` to represent the offset position of\na circle component. Touch events are processed with the\n[`pointerInput`](/reference/kotlin/androidx/compose/ui/input/pointer/package-summary#(androidx.compose.ui.Modifier).pointerInput(kotlin.Any,%20kotlin.coroutines.SuspendFunction1))\nmodifier. When we detect a new tap event, we call `animateTo` to animate the\noffset value to the tap position. A tap event can happen during the animation\ntoo, and in that case, `animateTo` interrupts the ongoing animation and starts\nthe animation to the new target position while maintaining the velocity of the\ninterrupted animation.\n\n\n```kotlin\n@Composable\nfun Gesture() {\n val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }\n Box(\n modifier = Modifier\n .fillMaxSize()\n .pointerInput(Unit) {\n coroutineScope {\n while (true) {\n // Detect a tap event and obtain its position.\n awaitPointerEventScope {\n val position = awaitFirstDown().position\n\n launch {\n // Animate to the tap position.\n offset.animateTo(position)\n }\n }\n }\n }\n }\n ) {\n Circle(modifier = Modifier.offset { offset.value.toIntOffset() })\n }\n}\n\nprivate fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())https://github.com/android/snippets/blob/dd30aee903e8c247786c064faab1a9ca8d10b46e/compose/snippets/src/main/java/com/example/compose/snippets/animations/AdvancedAnimationSnippets.kt#L62-L88\n```\n\n\u003cbr /\u003e\n\nAnother frequent pattern is we need to synchronize animation values with values\ncoming from touch events, such as drag. In the example below, we see \"swipe to\ndismiss\" implemented as a `Modifier` (rather than using the\n[`SwipeToDismiss`](/reference/kotlin/androidx/compose/material/package-summary#SwipeToDismiss(androidx.compose.material.DismissState,%20androidx.compose.ui.Modifier,%20kotlin.collections.Set,%20kotlin.Function1,%20kotlin.Function1,%20kotlin.Function1))\ncomposable). The horizontal offset of the element is represented as an\n`Animatable`. This API has a characteristic useful in gesture animation. Its\nvalue can be changed by touch events as well as the animation. When we receive a\ntouch down event, we stop the `Animatable` by the `stop` method so that any\nongoing animation is intercepted.\n\nDuring a drag event, we use `snapTo` to update the `Animatable` value with the\nvalue calculated from touch events. For fling, Compose provides\n`VelocityTracker` to record drag events and calculate velocity. The velocity can\nbe fed directly to `animateDecay` for the fling animation. When we want to slide\nthe offset value back to the original position, we specify the target offset\nvalue of `0f` with the `animateTo` method.\n\n\n```kotlin\nfun Modifier.swipeToDismiss(\n onDismissed: () -\u003e Unit\n): Modifier = composed {\n val offsetX = remember { Animatable(0f) }\n pointerInput(Unit) {\n // Used to calculate fling decay.\n val decay = splineBasedDecay\u003cFloat\u003e(this)\n // Use suspend functions for touch events and the Animatable.\n coroutineScope {\n while (true) {\n val velocityTracker = VelocityTracker()\n // Stop any ongoing animation.\n offsetX.stop()\n awaitPointerEventScope {\n // Detect a touch down event.\n val pointerId = awaitFirstDown().id\n\n horizontalDrag(pointerId) { change -\u003e\n // Update the animation value with touch events.\n launch {\n offsetX.snapTo(\n offsetX.value + change.positionChange().x\n )\n }\n velocityTracker.addPosition(\n change.uptimeMillis,\n change.position\n )\n }\n }\n // No longer receiving touch events. Prepare the animation.\n val velocity = velocityTracker.calculateVelocity().x\n val targetOffsetX = decay.calculateTargetValue(\n offsetX.value,\n velocity\n )\n // The animation stops when it reaches the bounds.\n offsetX.updateBounds(\n lowerBound = -size.width.toFloat(),\n upperBound = size.width.toFloat()\n )\n launch {\n if (targetOffsetX.absoluteValue \u003c= size.width) {\n // Not enough velocity; Slide back.\n offsetX.animateTo(\n targetValue = 0f,\n initialVelocity = velocity\n )\n } else {\n // The element was swiped away.\n offsetX.animateDecay(velocity, decay)\n onDismissed()\n }\n }\n }\n }\n }\n .offset { IntOffset(offsetX.value.roundToInt(), 0) }\n}https://github.com/android/snippets/blob/dd30aee903e8c247786c064faab1a9ca8d10b46e/compose/snippets/src/main/java/com/example/compose/snippets/animations/AdvancedAnimationSnippets.kt#L94-L152\n```\n\n\u003cbr /\u003e\n\nRecommended for you\n-------------------\n\n- Note: link text is displayed when JavaScript is off\n- [Value-based animations](/develop/ui/compose/animation/value-based)\n- [Drag, swipe, and fling](/develop/ui/compose/touch-input/pointer-input/drag-swipe-fling)\n- [Understand gestures](/develop/ui/compose/touch-input/pointer-input/understand-gestures)"]]