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