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

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

في المثال أدناه، نستخدم 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. تتميز واجهة برمجة التطبيقات هذه بسمة مفيدة في الرسوم المتحرّكة للّمسات. ويمكن تغيير قيمته بواسطة أحداث اللمس بالإضافة إلى الحركة. عندما نتلقّى حدث touch down، نوقف 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) }
}