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

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

בדוגמה שלמטה, אנחנו משתמשים ב-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 composable). ההיסט האופקי של הרכיב מיוצג כ-Animatable. ל-API הזה יש מאפיין שימושי באנימציה של תנועות. אפשר לשנות את הערך שלו באמצעות אירועי מגע וגם באמצעות האנימציה. כשמתקבל אירוע של מגע, אנחנו מפסיקים את Animatable באמצעות השיטה stop כדי ליירט כל אנימציה שמתבצעת.

במהלך אירוע גרירה, אנחנו משתמשים ב-snapTo כדי לעדכן את הערך Animatable עם הערך שמחושב מאירועי מגע. ב-Compose, כדי להשתמש בתנועת ה-fling, צריך להשתמש ב-VelocityTracker כדי לתעד אירועי גרירה ולחשב את המהירות. אפשר להזין את המהירות ישירות ל-animateDecay עבור אנימציית ההטלה. כדי להחזיר את ערך ההיסט למצב המקורי, מציינים את ערך היעד של ההיסט, 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) }
}