مثال پویانمایی پیشرفته: اشاره‌ها

وقتی با رویدادهای لمسی و پویانمایی‌ها کار می‌کنیم، در مقایسه با زمانی که فقط با پویانمایی‌ها کار می‌کنیم، چند چیز را باید درنظر بگیریم. اول از همه، ممکن است نیاز داشته باشیم که وقتی رویدادهای لمسی شروع می‌شود، پویانمایی درحال اجرا را متوقف کنیم، زیرا تعامل کاربر باید بالاترین اولویت را داشته باشد.

در مثال زیر، از 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 برای پویانمایی پرتاب تغذیه شود. وقتی بخواهیم مقدار جابه‌جایی را به موقعیت اصلی برگردانیم، مقدار جابه‌جایی هدف 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) }
}