Contoh animasi lanjutan: Gestur

Ada beberapa hal yang perlu dipertimbangkan saat mengerjakan peristiwa sentuh dan animasi, dibandingkan saat kita menangani animasi saja. Terlebih dahulu, kita mungkin perlu menghentikan animasi yang sedang berlangsung saat peristiwa sentuh dimulai karena interaksi pengguna harus diutamakan.

Pada contoh di bawah, kita menggunakan Animatable untuk mewakili posisi offset komponen lingkaran. Peristiwa sentuh diproses dengan pengubah pointerInput. Saat mendeteksi peristiwa ketuk baru, kita memanggil animateTo untuk menganimasi nilai offset ke posisi ketuk. Peristiwa ketuk juga dapat terjadi selama animasi, dan dalam hal ini, animateTo mengganggu animasi yang sedang berlangsung dan memulai animasi ke posisi target baru sambil mempertahankan kecepatan animasi yang terganggu.

@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())

Pola lain yang sering terjadi adalah kita perlu menyinkronkan nilai animasi dengan nilai yang berasal dari peristiwa sentuh, seperti tarik. Pada contoh di bawah, kita melihat "geser untuk menutup" diterapkan sebagai Modifier (bukan menggunakan composable SwipeToDismiss). Offset horizontal elemen direpresentasikan sebagai Animatable. API ini memiliki karakteristik yang berguna dalam animasi gestur. Nilainya dapat diubah oleh peristiwa sentuh serta animasi. Saat menerima peristiwa sentuhan, kita menghentikan Animatable dengan metode stop, sehingga setiap animasi yang sedang berjalan terhenti.

Selama peristiwa tarik, kita menggunakan snapTo untuk memperbarui nilai Animatable dengan nilai yang dihitung dari peristiwa sentuh. Untuk fling, Compose menyediakan VelocityTracker untuk merekam peristiwa tarik dan menghitung kecepatan. Kecepatan dapat diumpankan langsung ke animateDecay untuk animasi fling. Saat ingin menggeser nilai offset kembali ke posisi semula, kita menentukan nilai offset target 0f dengan metode 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) }
}