Przykład zaawansowanej animacji: gesty

Gdy pracujemy ze zdarzeniami dotyku i animacjami, trzeba wziąć pod uwagę kilka kwestii niż w przypadku samych animacji. Przede wszystkim konieczne może być przerwanie trwającej animacji po rozpoczęciu zdarzenia dotknięcia, ponieważ interakcja użytkownika powinna mieć najwyższy priorytet.

W przykładzie poniżej użyto elementu Animatable do oznaczenia pozycji przesunięcia komponentu okręgu. Zdarzenia kliknię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. Dotknięcie może też mieć miejsce w trakcie animacji. W takim przypadku animateTo przerywa trwającą animację i uruchamia ją do nowej pozycji docelowej, zachowując przy tym tempo przerywanej 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())

Częstym wzorcem jest synchronizowanie wartości animacji z wartościami pochodzącymi ze zdarzeń dotknięcia, takich jak przeciąganie. W przykładzie poniżej widzimy działanie funkcji „przesuń, aby zamknąć” zaimplementowaną jako Modifier (a nie za pomocą funkcji kompozycyjnej SwipeToDismiss). Przesunięcie w poziomie elementu jest przedstawiane jako Animatable. Ten interfejs API ma cechę przydatną w animacji gestów. Jej wartość można zmieniać za pomocą zdarzeń dotknięcia i animacji. Po otrzymaniu zdarzenia dotknięcia zatrzymujemy metodę Animatable za pomocą metody stop, aby umożliwić przechwycenie trwającej animacji.

W trakcie zdarzenia przeciągania używamy parametru snapTo, aby zaktualizować wartość Animatable o wartość obliczoną na podstawie zdarzeń dotknięcia. W przypadku przesuwania funkcja tworzenia udostępnia VelocityTracker do rejestrowania zdarzeń przeciągania i obliczania prędkości. Prędkość można przesłać bezpośrednio do obiektu animateDecay na potrzeby animacji rzutu. Gdy chcemy przesunąć wartość przesunięcia z powrotem do pozycji pierwotnej, określamy wartość przesunięcia docelowego 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) }
}