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

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

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