Przykład zaawansowanej animacji: gesty

Gdy pracujemy ze zdarzeniami dotknięcia i animacjami, musimy wziąć pod uwagę kilka kwestii, których nie musimy uwzględniać, gdy pracujemy tylko z animacjami. Po pierwsze, może być konieczne przerwanie trwającej animacji, gdy zaczynają się zdarzenia dotknięcia, ponieważ interakcja użytkownika powinna mieć najwyższy priorytet.

W przykładzie poniżej używamy Animatable do reprezentowania pozycji przesunięcia komponentu koła. Zdarzenia dotknięcia 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 kliknięcia może też wystąpić podczas animacji. W takim przypadku animateTo przerywa bieżącą animację i rozpoczyna animację do nowej pozycji docelowej, zachowując szybkość 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ągnięcie. W poniższym przykładzie widzimy gest „przesuń, aby zamknąć” zaimplementowany jako Modifier (zamiast użycia funkcji kompozycyjnej SwipeToDismiss). Przesunięcie poziome elementu jest reprezentowane jako Animatable. Ten interfejs API ma cechę przydatną w animacji gestów. Jego wartość można zmienić za pomocą zdarzeń dotknięcia oraz animacji. Gdy otrzymamy zdarzenie dotknięcia, zatrzymujemy Animatable za pomocą metody stop, aby przechwycić każdą trwającą animację.

Podczas zdarzenia przeciągania używamy snapTo, aby zaktualizować wartość Animatable za pomocą wartości obliczonej na podstawie zdarzeń dotknięcia. W przypadku rzutu Compose udostępnia VelocityTracker do rejestrowania zdarzeń przeciągania i obliczania szybkości. Szybkość można przekazać 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) }
}