مثال على الرسوم المتحركة المتقدمة: الإيماءات

هناك عدة أمور يجب أخذها في الاعتبار عند التعامل مع أحداث اللمس والرسوم المتحركة، مقارنةً بالتعامل مع الرسوم المتحركة وحدها. في البداية، قد نحتاج إلى إيقاف حركة رسوم متحركة مستمرة عندما تبدأ أحداث اللمس لأنّ تفاعل المستخدم يجب أن يكون له الأولوية القصوى.

في المثال أدناه، نستخدم 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) }
}