Contoh animasi lanjutan: Gestur
Tetap teratur dengan koleksi
Simpan dan kategorikan konten berdasarkan preferensi Anda.
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) }
}
Direkomendasikan untuk Anda
Konten dan contoh kode di halaman ini tunduk kepada lisensi yang dijelaskan dalam Lisensi Konten. Java dan OpenJDK adalah merek dagang atau merek dagang terdaftar dari Oracle dan/atau afiliasinya.
Terakhir diperbarui pada 2025-08-27 UTC.
[[["Mudah dipahami","easyToUnderstand","thumb-up"],["Memecahkan masalah saya","solvedMyProblem","thumb-up"],["Lainnya","otherUp","thumb-up"]],[["Informasi yang saya butuhkan tidak ada","missingTheInformationINeed","thumb-down"],["Terlalu rumit/langkahnya terlalu banyak","tooComplicatedTooManySteps","thumb-down"],["Sudah usang","outOfDate","thumb-down"],["Masalah terjemahan","translationIssue","thumb-down"],["Masalah kode / contoh","samplesCodeIssue","thumb-down"],["Lainnya","otherDown","thumb-down"]],["Terakhir diperbarui pada 2025-08-27 UTC."],[],[],null,["There are several things we have to take into consideration when we are working\nwith touch events and animations, compared to when we are working with\nanimations alone. First of all, we might need to interrupt an ongoing animation\nwhen touch events begin as user interaction should have the highest priority.\n\nIn the example below, we use an `Animatable` to represent the offset position of\na circle component. Touch events are processed with the\n[`pointerInput`](/reference/kotlin/androidx/compose/ui/input/pointer/package-summary#(androidx.compose.ui.Modifier).pointerInput(kotlin.Any,%20kotlin.coroutines.SuspendFunction1))\nmodifier. When we detect a new tap event, we call `animateTo` to animate the\noffset value to the tap position. A tap event can happen during the animation\ntoo, and in that case, `animateTo` interrupts the ongoing animation and starts\nthe animation to the new target position while maintaining the velocity of the\ninterrupted animation.\n\n\n```kotlin\n@Composable\nfun Gesture() {\n val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) }\n Box(\n modifier = Modifier\n .fillMaxSize()\n .pointerInput(Unit) {\n coroutineScope {\n while (true) {\n // Detect a tap event and obtain its position.\n awaitPointerEventScope {\n val position = awaitFirstDown().position\n\n launch {\n // Animate to the tap position.\n offset.animateTo(position)\n }\n }\n }\n }\n }\n ) {\n Circle(modifier = Modifier.offset { offset.value.toIntOffset() })\n }\n}\n\nprivate fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())https://github.com/android/snippets/blob/5673ffc60b614daf028ee936227128eb8c4f9781/compose/snippets/src/main/java/com/example/compose/snippets/animations/AdvancedAnimationSnippets.kt#L62-L88\n```\n\n\u003cbr /\u003e\n\nAnother frequent pattern is we need to synchronize animation values with values\ncoming from touch events, such as drag. In the example below, we see \"swipe to\ndismiss\" implemented as a `Modifier` (rather than using the\n[`SwipeToDismiss`](/reference/kotlin/androidx/compose/material/package-summary#SwipeToDismiss(androidx.compose.material.DismissState,%20androidx.compose.ui.Modifier,%20kotlin.collections.Set,%20kotlin.Function1,%20kotlin.Function1,%20kotlin.Function1))\ncomposable). The horizontal offset of the element is represented as an\n`Animatable`. This API has a characteristic useful in gesture animation. Its\nvalue can be changed by touch events as well as the animation. When we receive a\ntouch down event, we stop the `Animatable` by the `stop` method so that any\nongoing animation is intercepted.\n\nDuring a drag event, we use `snapTo` to update the `Animatable` value with the\nvalue calculated from touch events. For fling, Compose provides\n`VelocityTracker` to record drag events and calculate velocity. The velocity can\nbe fed directly to `animateDecay` for the fling animation. When we want to slide\nthe offset value back to the original position, we specify the target offset\nvalue of `0f` with the `animateTo` method.\n\n\n```kotlin\nfun Modifier.swipeToDismiss(\n onDismissed: () -\u003e Unit\n): Modifier = composed {\n val offsetX = remember { Animatable(0f) }\n pointerInput(Unit) {\n // Used to calculate fling decay.\n val decay = splineBasedDecay\u003cFloat\u003e(this)\n // Use suspend functions for touch events and the Animatable.\n coroutineScope {\n while (true) {\n val velocityTracker = VelocityTracker()\n // Stop any ongoing animation.\n offsetX.stop()\n awaitPointerEventScope {\n // Detect a touch down event.\n val pointerId = awaitFirstDown().id\n\n horizontalDrag(pointerId) { change -\u003e\n // Update the animation value with touch events.\n launch {\n offsetX.snapTo(\n offsetX.value + change.positionChange().x\n )\n }\n velocityTracker.addPosition(\n change.uptimeMillis,\n change.position\n )\n }\n }\n // No longer receiving touch events. Prepare the animation.\n val velocity = velocityTracker.calculateVelocity().x\n val targetOffsetX = decay.calculateTargetValue(\n offsetX.value,\n velocity\n )\n // The animation stops when it reaches the bounds.\n offsetX.updateBounds(\n lowerBound = -size.width.toFloat(),\n upperBound = size.width.toFloat()\n )\n launch {\n if (targetOffsetX.absoluteValue \u003c= size.width) {\n // Not enough velocity; Slide back.\n offsetX.animateTo(\n targetValue = 0f,\n initialVelocity = velocity\n )\n } else {\n // The element was swiped away.\n offsetX.animateDecay(velocity, decay)\n onDismissed()\n }\n }\n }\n }\n }\n .offset { IntOffset(offsetX.value.roundToInt(), 0) }\n}https://github.com/android/snippets/blob/5673ffc60b614daf028ee936227128eb8c4f9781/compose/snippets/src/main/java/com/example/compose/snippets/animations/AdvancedAnimationSnippets.kt#L94-L152\n```\n\n\u003cbr /\u003e\n\nRecommended for you\n\n- Note: link text is displayed when JavaScript is off\n- [Value-based animations](/develop/ui/compose/animation/value-based)\n- [Drag, swipe, and fling](/develop/ui/compose/touch-input/pointer-input/drag-swipe-fling)\n- [Understand gestures](/develop/ui/compose/touch-input/pointer-input/understand-gestures)"]]