Przykład zaawansowanej animacji: gesty

Podczas pracy z zdarzeniami dotykowymi i animacjami musimy wziąć pod uwagę kilka kwestii, w porównaniu z pracą z samymi animacjami. Po pierwsze, możemy potrzebować przerwać trwającą animację, gdy zaczną się zdarzenia dotknięcia, ponieważ interakcja użytkownika powinna mieć najwyższy priorytet.

W przykładzie poniżej używamy znaku Animatable, aby przedstawić pozycję przesunięcia komponentu koła. Zdarzenia dotyku są przetwarzane za pomocą modyfikatora pointerInput. Gdy wykryjemy nowe zdarzenie kliknięcia, wywołujemy funkcję animateTo, aby animować wartość przesunięcia do pozycji kliknięcia. Zdarzenie dotknięcia może wystąpić również podczas animacji. W takim przypadku funkcja animateTo przerywa trwającą animację i uruchamia 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 synchronizacja wartości animacji z wartościami pochodzącymi ze zdarzeń dotykowych, takich jak przeciąganie. W poniższym przykładzie widzimy, że „przesuń, aby zamknąć” zostało zaimplementowane jako Modifier (zamiast używać komponentu SwipeToDismiss). Poziome przesunięcie elementu jest reprezentowane jako Animatable. Ten interfejs API ma cechę przydatną w animacji gestów. Jego wartość można zmieniać za pomocą zdarzeń dotykowych, a także animacji. Gdy otrzymamy zdarzenie naciśnięcia, zatrzymujemy Animatable za pomocą metody stop, aby przerwać trwającą animację.

Podczas zdarzenia przeciągania używamy funkcji snapTo, aby zaktualizować wartość Animatable o wartość obliczoną na podstawie zdarzeń dotykowych. W przypadku gestu przesuwania Compose udostępnia VelocityTracker do rejestrowania zdarzeń przeciągania i obliczania prędkości. Prędkość można przekazywać bezpośrednio do animateDecay na potrzeby animacji rzutu. Jeśli 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) }
}