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

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

בדוגמה הבאה אנחנו משתמשים ב-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 עם ערך שחושב מאירועי מגע. להחלקה, 'כתיבה' מספקת 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) }
}