Przykład zaawansowanej animacji: gesty

Podczas pracy z animacjami i zdarzeniami dotykowymi należy wziąć pod uwagę kilka kwestii, których nie trzeba uwzględniać, gdy pracuje się tylko z animacjami. Po pierwsze, może być konieczne przerwanie trwającej animacji, gdy zaczynają się zdarzenia dotykowe, ponieważ interakcja użytkownika powinna mieć najwyższy priorytet.

W poniższym przykładzie używamy elementu Animatable, aby reprezentować przesunięcie pozycji komponentu koła. Zdarzenia dotyku są przetwarzane z modyfikatorem pointerInput. Gdy wykryjemy nowe zdarzenie dotknięcia, wywołujemy funkcję animateTo, aby animować wartość przesunięcia do pozycji dotknięcia. Zdarzenie dotknięcia może też wystąpić w trakcie animacji. W takim przypadku animateToprzerywa bieżą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 konieczność synchronizowania wartości animacji z wartościami pochodzącymi ze zdarzeń dotykowych, takich jak przeciąganie. W przykładzie poniżej widać funkcję „przesuń, aby zamknąć” zaimplementowaną jako Modifier (zamiast komponentu SwipeToDismiss). Poziome przesunięcie elementu jest reprezentowane przez Animatable. Ten interfejs API ma cechę przydatną w animacji gestów. Jego wartość może być zmieniana przez zdarzenia dotykowe i animację. Gdy otrzymamy zdarzenie dotknięcia, zatrzymujemy Animatable za pomocą metody stop, aby przechwycić trwającą animację.

Podczas zdarzenia przeciągania używamy funkcji snapTo, aby zaktualizować wartość Animatable za pomocą wartości obliczonej na podstawie zdarzeń dotyku. W przypadku przesunięcia Compose udostępnia VelocityTracker do rejestrowania zdarzeń przeciągania i obliczania prędkości. Prędkość można przesłać bezpośrednio do animateDecay w ramach animacji rzutu. 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) }
}