דוגמה לאנימציה מתקדמת: תנועות

יש כמה דברים שצריך להביא בחשבון כשעובדים עם אירועי מגע ואנימציות, בהשוואה לעבודה עם אנימציות בלבד. קודם כל, ייתכן שנצטרך לקטוע אנימציה מתמשכת כשאירועי מגע מתחילים, כי האינטראקציה של המשתמש צריכה להיות בעדיפות הגבוהה ביותר.

בדוגמה הבאה אנחנו משתמשים ב-Animatable כדי לייצג את מיקום ההיסט של רכיב מעגל. אירועי המגע עוברים עיבוד באמצעות המשתנה המשנה pointerInput. כשאנחנו מזהים אירוע הקשה חדש, אנחנו קוראים ל-animateTo כדי ליצור אנימציה של ערך ההיסט למיקום ההקשה. אירוע הקשה יכול להתרחש גם במהלך האנימציה, ובמקרה כזה, animateTo משבית את האנימציה המתמשכת ומתחיל את האנימציה למיקום היעד החדש תוך שמירה על המהירות של האנימציה שהושבתה.

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

דפוס נפוץ נוסף הוא שצריך לסנכרן ערכים של אנימציה עם ערכים שמגיעים מאירועי מגע, כמו גרירה. בדוגמה הבאה אנחנו רואים שהפעולה 'החלקה כדי לסגור' מוטמעת בתור Modifier (במקום להשתמש בתוכן הקומפוזבילי SwipeToDismiss). ההיסט האופקי של הרכיב מיוצג באמצעות Animatable. לממשק ה-API הזה יש מאפיין שימושי באנימציה של תנועות. אפשר לשנות את הערך שלו באמצעות אירועי מגע וגם באמצעות האנימציה. כשאנחנו מקבלים אירוע של מגע, אנחנו מפסיקים את Animatable באמצעות השיטה stop כדי שתתבצע יירוט של כל אנימציה מתמשכת.

במהלך אירוע גרירה, אנחנו משתמשים ב-snapTo כדי לעדכן את הערך של Animatable בערך שמחושב מאירועי מגע. לתנועה מהירה, Compose מספק את הפונקציה VelocityTracker כדי לתעד אירועי גרירה ולחשב את המהירות. אפשר להעביר את המהירות ישירות אל animateDecay עבור אנימציית ה-fling. כשרוצים להחזיר את ערך ההיסט למיקום המקורי, מציינים את ערך ההיסט היעד של 0f באמצעות השיטה animateTo.

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