Beispiel für eine erweiterte Animation: Gesten

Wenn wir mit Touch-Events und Animationen arbeiten, müssen wir einige Dinge beachten, als wenn wir nur mit Animationen arbeiten. Zunächst müssen wir möglicherweise eine laufende Animation unterbrechen, wenn Touch-Ereignisse beginnen, da Nutzerinteraktionen die höchste Priorität haben sollten.

Im folgenden Beispiel wird ein Animatable verwendet, um die Versatzposition einer Kreiskomponente darzustellen. Touch-Ereignisse werden mit dem Modifikator pointerInput verarbeitet. Wird ein neues Tippereignis erkannt, rufen wir animateTo auf, um den Versatzwert zur Tippposition zu animieren. Auch während der Animation kann ein Tippereignis stattfinden. In diesem Fall unterbricht animateTo die laufende Animation und startet sie an der 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 von Touch-Ereignissen wie Ziehbewegungen. Im folgenden Beispiel 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 eine Eigenschaft, die für die Bewegungsanimation nützlich ist. Sein Wert kann durch Touch-Ereignisse und die Animation geändert werden. Wenn wir ein Touchdown-Ereignis erhalten, stoppen wir Animatable mit der Methode stop, damit alle laufenden Animationen abgefangen werden.

Während eines Drag-Ereignisses verwenden wir snapTo, um den Animatable-Wert mit dem Wert zu aktualisieren, der aus Touch-Ereignissen berechnet wurde. Für das Fling bietet Compose VelocityTracker zum Aufzeichnen von Drag-Events und zur Berechnung der Geschwindigkeit. Die Geschwindigkeit kann für die Fluganimation direkt animateDecay zugeführt werden. Wenn wir den Offset-Wert zurück zur ursprünglichen Position verschieben möchten, geben wir den Offset-Zielwert von 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) }
}