Beispiel für eine erweiterte Animation: Gesten

Bei der Arbeit mit Touch-Ereignissen und Animationen müssen wir mehrere Dinge beachten, die bei der Arbeit mit reinen Animationen nicht relevant sind. Erstens: Möglicherweise müssen wir eine laufende Animation unterbrechen, wenn Touch-Ereignisse beginnen, da die Nutzerinteraktion die höchste Priorität haben sollte.

Im folgenden Beispiel wird mit einem Animatable die Offset-Position einer Kreiskomponente dargestellt. Touch-Ereignisse werden mit dem Modifikator pointerInput verarbeitet. Wenn wir ein neues Tippen-Ereignis erkennen, wird animateTo aufgerufen, um den Offset-Wert an die Tippposition zu animieren. Ein Tipp-Ereignis 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 synchronisieren müssen, z. B. Ziehen. Im folgenden Beispiel wird „Wischen zum Schließen“ als Modifier implementiert, anstatt die SwipeToDismiss-Komposition zu verwenden. Der horizontale Offset des Elements wird als Animatable dargestellt. Diese API hat eine Eigenschaft, die sich für die Gestenanimation eignet. Der Wert kann durch Touch-Ereignisse und die Animation geändert werden. Wenn wir ein Touchdown-Ereignis erhalten, stoppen wir die Animatable mit der Methode stop, damit laufende Animationen abgefangen werden.

Während eines Drag-Ereignisses wird mit snapTo der Wert Animatable mit dem Wert aktualisiert, der aus Touch-Ereignissen berechnet wird. Für Wischaktionen bietet Compose VelocityTracker, um Ziegereignisse aufzuzeichnen und die Geschwindigkeit zu berechnen. Die Geschwindigkeit kann direkt an animateDecay für die Wischanimation übergeben werden. Wenn wir den Offsetwert wieder in die ursprüngliche Position schieben möchten, geben wir mit der Methode animateTo den Zieloffsetwert 0f 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) }
}