مثال انیمیشن پیشرفته: ژست ها

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

در مثال زیر، ما از یک Animatable برای نمایش موقعیت آفست یک کامپوننت دایره‌ای استفاده می‌کنیم. رویدادهای لمسی با اصلاح‌کننده pointerInput پردازش می‌شوند. وقتی یک رویداد tap جدید را تشخیص می‌دهیم، animateTo برای متحرک‌سازی مقدار آفست به موقعیت tap فراخوانی می‌کنیم. یک رویداد tap می‌تواند در طول انیمیشن نیز اتفاق بیفتد و در این صورت، 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())

الگوی رایج دیگر این است که ما باید مقادیر انیمیشن را با مقادیری که از رویدادهای لمسی، مانند کشیدن، می‌آیند، همگام‌سازی کنیم. در مثال زیر، می‌بینیم که "swipe to dismiss" به عنوان یک Modifier پیاده‌سازی شده است (به جای استفاده از SwipeToDismiss composable). جابجایی افقی عنصر به عنوان یک Animatable نمایش داده می‌شود. این API یک ویژگی مفید در انیمیشن‌های حرکتی دارد. مقدار آن می‌تواند توسط رویدادهای لمسی و همچنین انیمیشن تغییر کند. وقتی یک رویداد touch down دریافت می‌کنیم، Animatable را با استفاده از متد stop متوقف می‌کنیم تا هرگونه انیمیشن در حال انجام متوقف شود.

در طول یک رویداد drag، ما از snapTo برای به‌روزرسانی مقدار Animatable با مقدار محاسبه‌شده از رویدادهای touch استفاده می‌کنیم. برای fling، Compose از VelocityTracker برای ثبت رویدادهای drag و محاسبه سرعت استفاده می‌کند. سرعت را می‌توان مستقیماً به animateDecay برای انیمیشن fling ارسال کرد. وقتی می‌خواهیم مقدار offset را به موقعیت اصلی برگردانیم، مقدار offset هدف 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) }
}

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}