Beispiel für eine erweiterte Animation: Gesten

Im Vergleich zur Arbeit mit Animationen allein müssen wir bei der Arbeit mit Touch-Ereignissen und Animationen mehrere Dinge berücksichtigen. Zunächst müssen wir möglicherweise eine laufende Animation unterbrechen, wenn Touch-Ereignisse beginnen, da die Nutzerinteraktion die höchste Priorität haben sollte.

Im folgenden Beispiel verwenden wir ein Animatable, um die Offsetposition einer Kreis-Komponente darzustellen. Touch-Ereignisse werden mit dem pointerInput Modifikator verarbeitet. Wenn wir ein neues Tippereignis erkennen, rufen wir animateTo auf, um den Offsetwert auf die Tippposition zu animieren. Ein Tippereignis kann auch während der Animation auftreten. In diesem Fall unterbricht animateTo die laufende Animation und startet die Animation zur neuen Zielposition, wobei die Geschwindigkeit der unterbrochenen Animation beibehalten wird.

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

Ein weiteres häufiges Muster ist, dass wir Animationswerte mit Werten aus Touch-Ereignissen wie Drag-Ereignissen synchronisieren müssen. Im folgenden Beispiel wird „Zum Schließen wischen“ als Modifier implementiert (anstatt die SwipeToDismiss zusammensetzbare Funktion zu verwenden). Der horizontale Offset des Elements wird als Animatable dargestellt. Diese API hat eine Eigenschaft, die für Gestenanimationen nützlich ist. Ihr Wert kann sowohl durch Touch-Ereignisse als auch durch die Animation geändert werden. Wenn wir ein Touch-Down-Ereignis empfangen, beenden wir das Animatable mit der Methode stop, damit alle laufenden Animationen unterbrochen werden.

Bei einem Drag-Ereignis verwenden wir snapTo, um den Animatable-Wert mit dem aus Touch-Ereignissen berechneten Wert zu aktualisieren. Für Fling stellt Compose VelocityTracker zur Verfügung, um Drag-Ereignisse aufzuzeichnen und die Geschwindigkeit zu berechnen. Die Geschwindigkeit kann direkt an animateDecay für die Fling-Animation übergeben werden. Wenn wir den Offsetwert wieder auf die ursprüngliche Position verschieben möchten, geben wir den Zieloffsetwert 0f mit der Methode animateTo an.

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) }
}