與單獨處理動畫相比,同時處理觸控事件和動畫時,必須考慮幾個事項。首先,在觸控事件開始時,我們可能必須中斷處理中的動畫,因為使用者互動事件應該擁有最高的優先順序。
在以下範例中,我們使用 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) } }