高级动画示例:手势

与单独处理动画相比,当我们处理触摸事件和动画时,必须考虑几个事项。首先,当触摸事件开始时,我们可能需要中断正在播放的动画,因为用户互动应当具有最高优先级。

在下面的示例中,我们使用 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,以便拦截任何正在播放的动画。

在拖动事件期间,我们使用 snapToAnimatable 值更新为从触摸事件计算得出的值。对于快速滑动,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) } }