При работе с сенсорными событиями и анимацией необходимо учитывать несколько моментов, в отличие от работы только с анимацией. Прежде всего, нам может потребоваться прерывать текущую анимацию при начале сенсорного события, поскольку взаимодействие с пользователем должно иметь наивысший приоритет.
В примере ниже мы используем 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) } }
Рекомендовано для вас
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Анимации, основанные на ценностях
- Перетаскивайте, проведите пальцем и швыряйте
- Понимать жесты