与仅使用动画相比,在处理触摸事件和动画时,我们需要考虑一些事情。首先,当触摸事件开始时,我们可能需要中断正在进行的动画,因为用户交互应该具有最高优先级。
在下面的示例中,我们使用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) } }