Расширенный пример анимации: жесты

Есть несколько вещей, которые мы должны учитывать, когда работаем с сенсорными событиями и анимацией, по сравнению с тем, когда мы работаем только с анимацией. Прежде всего, нам может потребоваться прервать текущую анимацию, когда начинаются события касания, поскольку взаимодействие с пользователем должно иметь наивысший приоритет.

В приведенном ниже примере мы используем 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 имеет особенность, полезную для анимации жестов. Его значение можно изменить с помощью событий касания, а также анимации. Когда мы получаем событие приземления, мы останавливаем Animatable с помощью метода stop , чтобы любая текущая анимация была перехвачена.

Во время события перетаскивания мы используем snapTo чтобы обновить значение Animatable значением, рассчитанным на основе событий касания. Для броска Compose предоставляет VelocityTracker для записи событий перетаскивания и расчета скорости. Скорость можно передать непосредственно в animateDecay для анимации полета. Когда мы хотим сдвинуть значение смещения обратно в исходное положение, мы указываем целевое значение смещения 0f с помощью метода animateTo .

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) }
}

{% дословно %} {% дословно %} {% дословно %} {% дословно %}