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

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

در مثال زیر، از 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 نشان داده می‌شود. این میانای برنامه‌سازی کاربردی ویژگی‌ای دارد که در پویانمایی اشاره مفید است. مقدار آن را می‌توان با رویدادهای لمسی و همچنین پویانمایی تغییر داد. وقتی رویداد لمس کردن را دریافت می‌کنیم، 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) }
}