Beispiel für eine erweiterte Animation: Gesten

Wenn wir mit Touch-Events und Animationen arbeiten, müssen wir im Vergleich zur Arbeit mit Animationen allein mehrere Dinge berücksichtigen. Zuerst müssen wir möglicherweise eine laufende Animation unterbrechen, wenn Touch-Events beginnen, da die Nutzerinteraktion die höchste Priorität haben sollte.

Im folgenden Beispiel wird ein Animatable verwendet, um die Offset-Position einer Kreiskomponente darzustellen. Touch-Events werden mit dem Modifikator pointerInput verarbeitet. Wenn ein neues Tippereignis erkannt wird, rufen wir animateTo auf, um den Offsetwert zur 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 die Synchronisierung von Animationswerten mit Werten aus Touch-Events, z. B. Ziehen. Im folgenden Beispiel wird „Wischen zum Schließen“ als Modifier implementiert (anstatt das Compose-Element SwipeToDismiss zu verwenden). Der horizontale Versatz des Elements wird als Animatable dargestellt. Diese API hat eine Eigenschaft, die für Gestenanimationen nützlich ist. Der Wert kann sowohl durch Touch-Events als auch durch die Animation geändert werden. Wenn wir ein Touch-Down-Ereignis erhalten, beenden wir die Animatable mit der Methode stop, damit alle laufenden Animationen abgefangen werden.

Während eines Ziehereignisses verwenden wir snapTo, um den Wert Animatable mit dem aus Touch-Events berechneten Wert zu aktualisieren. Für das Schleudern bietet Compose VelocityTracker, um Ziehereignisse aufzuzeichnen und die Geschwindigkeit zu berechnen. Die Geschwindigkeit kann direkt an animateDecay für die Wisch-Animation übergeben werden. Wenn wir den Offset-Wert wieder auf die ursprüngliche Position zurücksetzen möchten, geben wir den Ziel-Offset-Wert 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) }
}