Przykład zaawansowanej animacji: gesty

W porównaniu z pracą z samymi animacjami, podczas pracy ze zdarzeniami dotknięcia i animacjami musimy wziąć pod uwagę kilka kwestii. Przede wszystkim, gdy zaczynają się zdarzenia dotknięcia, możemy przerwać trwającą animację, ponieważ interakcja z użytkownikiem powinna mieć najwyższy priorytet.

W poniższym przykładzie używamy Animatable do przedstawienia pozycji przesunięcia komponentu koła. Zdarzenia dotknięcia są przetwarzane za pomocą pointerInput modyfikatora. Gdy wykryjemy nowe zdarzenie dotknięcia, wywołujemy animateTo, aby animować wartość przesunięcia do pozycji dotknięcia. Zdarzenie dotknięcia może wystąpić również podczas animacji. W takim przypadku animateTo przerywa trwającą animację i rozpoczyna animację do nowej pozycji docelowej, zachowując prędkość przerwanej animacji.

@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())

Innym częstym wzorcem jest synchronizowanie wartości animacji z wartościami pochodzącymi ze zdarzeń dotknięcia, takich jak przeciąganie. W poniższym przykładzie widzimy, że "przesuń palcem aby zamknąć" jest zaimplementowane jako Modifier (zamiast używać elementu kompozycyjnego SwipeToDismiss). Poziome przesunięcie elementu jest przedstawione jako Animatable. Ten interfejs API ma cechę przydatną w animacji gestów. Jego wartość można zmieniać za pomocą zdarzeń dotknięcia oraz animacji. Gdy otrzymamy zdarzenie dotknięcia, zatrzymujemy Animatable za pomocą metody stop, aby przerwać trwającą animację.

Podczas zdarzenia przeciągania używamy snapTo, aby zaktualizować wartość Animatable wartością obliczoną na podstawie zdarzeń dotknięcia. W przypadku przeciągnięcia Compose udostępnia VelocityTracker do rejestrowania zdarzeń przeciągania i obliczania prędkości. Prędkość można bezpośrednio przekazać do animateDecay w celu animacji przeciągnięcia. Gdy chcemy przesunąć wartość przesunięcia z powrotem do pierwotnej pozycji, określamy docelową wartość przesunięcia 0f za pomocą metody 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) }
}