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

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

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