Beispiel für eine erweiterte Animation: Gesten

Wenn wir mit Touch-Ereignissen und -Animationen arbeiten, müssen wir einige Dinge beachten, wenn wir mit Animationen allein arbeiten. Zuerst muss möglicherweise eine laufende Animation unterbrochen werden, wenn Touchereignisse beginnen, da Nutzerinteraktionen die höchste Priorität haben sollten.

Im folgenden Beispiel verwenden wir ein Animatable, um die Versatzposition einer Kreiskomponente darzustellen. Touch-Ereignisse werden mit dem Modifizierer pointerInput verarbeitet. Wenn ein neues Tippereignis erkannt wird, rufen wir animateTo auf, um den Versatzwert zur Tippposition zu animieren. Auch während der Animation kann ein Tippereignis auftreten. In diesem Fall unterbricht animateTo die laufende Animation und startet die Animation an der neuen Zielposition, während 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äufig auftretendes Muster ist die Synchronisierung von Animationswerten mit Werten, die aus Touch-Ereignissen wie dem Ziehen stammen. Im Beispiel unten wird „Zum Schließen wischen“ als Modifier implementiert, anstatt die zusammensetzbare Funktion SwipeToDismiss zu verwenden. Der horizontale Versatz des Elements wird als Animatable dargestellt. Diese API hat ein Merkmal, das bei Gestenanimation nützlich ist. Der Wert kann durch Touch-Ereignisse und die Animation geändert werden. Wenn wir ein Touchdown-Ereignis erhalten, stoppen wir den Animatable mit der Methode stop, damit die laufende Animation abgefangen wird.

Während eines Drag-Ereignisses verwenden wir snapTo, um den Animatable-Wert mit dem aus Touch-Ereignissen berechneten Wert zu aktualisieren. Für das fling bietet Compose VelocityTracker an, um Drag-Ereignisse aufzuzeichnen und die Geschwindigkeit zu berechnen. Die Geschwindigkeit kann für die Schleuder-Animation direkt an animateDecay übergeben werden. Um den Offset-Wert zurück an die ursprüngliche Position zu verschieben, geben wir den Ziel-Offset-Wert 0f mit der animateTo-Methode 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) }
}