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

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

В примере ниже мы используем 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) }
}

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