बेहतर ऐनिमेशन का उदाहरण: हाथ के जेस्चर

टच इवेंट और ऐनिमेशन के साथ काम करते समय, हमें कई बातों का ध्यान रखना पड़ता है. हालांकि, सिर्फ़ ऐनिमेशन के साथ काम करते समय, हमें इन बातों का ध्यान नहीं रखना पड़ता. सबसे पहले, टच इवेंट शुरू होने पर, हो सकता है कि हमें चल रहे ऐनिमेशन में रुकावट डालनी पड़े, क्योंकि उपयोगकर्ता इंटरैक्शन को सबसे ज़्यादा प्राथमिकता दी जानी चाहिए.

नीचे दिए गए उदाहरण में, हमने सर्कल कॉम्पोनेंट की ऑफ़सेट पोज़िशन दिखाने के लिए 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 के तौर पर दिखाया जाता है. इस एपीआई में एक ऐसी सुविधा है जो जेस्चर ऐनिमेशन में काम की है. इसकी वैल्यू को टच इवेंट के साथ-साथ ऐनिमेशन की मदद से भी बदला जा सकता है. जब हमें टच डाउन इवेंट मिलता है, तो हम stop तरीके से Animatable को रोक देते हैं, ताकि चल रहे किसी भी ऐनिमेशन को रोका जा सके.

खींचने और छोड़ने के इवेंट के दौरान, हम snapTo का इस्तेमाल करके Animatable वैल्यू को अपडेट करते हैं. इसके लिए, टच इवेंट से मिली वैल्यू का इस्तेमाल किया जाता है. फ़्लिंग के लिए, Compose में VelocityTracker उपलब्ध है. इसकी मदद से, खींचने और छोड़ने के इवेंट रिकॉर्ड किए जा सकते हैं और वेग का हिसाब लगाया जा सकता है. फ़्लिंग ऐनिमेशन के लिए, वेग को सीधे animateDecay में डाला जा सकता है. जब हमें ऑफ़सेट वैल्यू को फिर से मूल स्थिति पर स्लाइड करना हो, तो हम animateTo तरीके से 0f की टारगेट ऑफ़सेट वैल्यू तय करते हैं.

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) }
}