1. Pengantar
Terakhir Diperbarui: 21-11-2023
Dalam codelab ini, Anda akan mempelajari cara menggunakan beberapa Animation API di Jetpack Compose.
Jetpack Compose adalah toolkit UI modern yang dirancang untuk menyederhanakan pengembangan UI. Jika Anda baru menggunakan Jetpack Compose, ada beberapa codelab yang mungkin ingin Anda coba sebelum codelab ini.
Yang akan Anda pelajari
- Cara menggunakan beberapa Animation API dasar
Prasyarat
- Pengetahuan Kotlin dasar
- Pengetahuan Compose dasar yang mencakup:
- Tata letak sederhana (Kolom, Baris, Kotak, dll.)
- Elemen UI sederhana (Tombol, Teks, dll.)
- Status dan rekomposisi
Yang Anda butuhkan
2. Mempersiapkan
Download kode codelab. Anda dapat meng-clone repositori sebagai berikut:
$ git clone https://github.com/android/codelab-android-compose.git
Atau, Anda dapat mendownload repositori sebagai file ZIP:
Impor project AnimationCodelab
di Android Studio.
Project ini memiliki beberapa modul di dalamnya:
start
adalah status awal codelab.finished
adalah status akhir aplikasi setelah menyelesaikan codelab ini.
Pastikan start
dipilih di menu dropdown untuk konfigurasi run.
Kita akan mulai mengerjakan beberapa skenario animasi di bab berikutnya. Setiap cuplikan kode yang kita kerjakan di codelab ini ditandai dengan komentar // TODO
. Salah satu trik yang rapi adalah membuka jendela alat TODO di Android Studio dan menavigasi setiap komentar TODO untuk bab tersebut.
3. Menganimasikan perubahan nilai sederhana
Mari kita mulai dengan salah satu API animasi paling sederhana di Compose: animate*AsState
API. API ini harus digunakan saat menganimasikan perubahan State
.
Jalankan konfigurasi start
dan coba beralih tab dengan mengklik tombol "Rumah" dan "Kantor" di bagian atas. Tindakan ini tidak benar-benar mengalihkan konten tab, namun Anda dapat melihat bahwa warna latar belakang konten berubah.
Klik TODO 1 di jendela alat TODO dan lihat bagaimana implementasinya. TODO ini berada di composable Home
.
val backgroundColor = if (tabPage == TabPage.Home) Seashell else GreenLight
Di sini, tabPage
adalah TabPage
yang didukung oleh objek State
. Bergantung pada nilainya, warna latar belakang dialihkan antara jingga pucat dan hijau. Kita ingin menganimasikan perubahan nilai ini.
Untuk menganimasikan perubahan nilai sederhana seperti ini, kita dapat menggunakan animate*AsState
API. Anda dapat membuat nilai animasi dengan menggabungkan nilai yang berubah dengan varian composable animate*AsState
yang sesuai, yaitu animateColorAsState
dalam kasus ini. Nilai yang ditampilkan adalah objek State<T>
, sehingga kita dapat menggunakan properti delegasi lokal dengan deklarasi by
untuk memperlakukannya seperti variabel normal.
val backgroundColor by animateColorAsState(
targetValue = if (tabPage == TabPage.Home) Seashell else GreenLight,
label = "background color")
Jalankan kembali aplikasi dan coba beralih tab. Perubahan warna sekarang dianimasikan.
4. Menganimasikan visibilitas
Jika Anda men-scroll konten aplikasi, Anda akan melihat tombol tindakan mengambang meluas dan menyusut, bergantung pada arah scroll Anda.
Temukan TODO 2-1 dan lihat cara kerjanya. TODO ini berada di composable HomeFloatingActionButton
. Teks yang bertuliskan "EDIT" ditampilkan atau disembunyikan menggunakan pernyataan if
.
if (extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
Menganimasikan perubahan visibilitas ini semudah mengganti if
dengan composable AnimatedVisibility
.
AnimatedVisibility(extended) {
Text(
text = stringResource(R.string.edit),
modifier = Modifier
.padding(start = 8.dp, top = 3.dp)
)
}
Jalankan aplikasi dan lihat bagaimana FAB meluas dan menyusut sekarang.
AnimatedVisibility
menjalankan animasinya setiap kali nilai Boolean
yang ditentukan berubah. Secara default, AnimatedVisibility
menampilkan elemen dengan memudar dan meluaskannya, serta menyembunyikannya dengan memudar dan menyusut. Perilaku ini sangat cocok untuk contoh ini dengan FAB, tetapi kita juga dapat menyesuaikan perilaku tersebut.
Coba klik FAB, dan Anda akan melihat pesan yang menyatakan "Edit feature is not supported" (Fitur edit tidak didukung). AnimatedVisibility
juga digunakan untuk menganimasikan muncul dan hilangnya kode. Berikutnya, Anda akan menyesuaikan perilaku ini sehingga pesan bergeser masuk dari atas, dan bergeser keluar ke atas.
Temukan TODO 2-2 dan lihat kode dalam composable EditMessage
.
AnimatedVisibility(
visible = shown
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Untuk menyesuaikan animasi, tambahkan parameter enter
dan exit
ke composable AnimatedVisibility
.
Parameter enter
harus berupa instance EnterTransition
. Untuk contoh ini, kita dapat menggunakan fungsi slideInVertically
untuk membuat EnterTransition
dan slideOutVertically
untuk transisi keluar. Ubah kode sebagai berikut:
AnimatedVisibility(
visible = shown,
enter = slideInVertically(),
exit = slideOutVertically()
)
Jalankan kembali aplikasi, dengan mengklik tombol edit, Anda mungkin melihat bahwa animasi terlihat lebih baik, tetapi tidak sepenuhnya benar, ini karena perilaku default slideInVertically
dan slideOutVertically
menggunakan setengah tinggi item.
Untuk transisi masuk: kita dapat menyesuaikan perilaku default untuk menggunakan seluruh tinggi item untuk menganimasikannya secara benar dengan menyetel parameter initialOffsetY
. initialOffsetY
harus berupa lambda yang menampilkan posisi awal.
Lambda menerima satu argumen, yaitu tinggi elemen. Untuk memastikan item bergeser masuk dari atas layar, kita menampilkan nilai negatifnya karena nilai bagian atas layar memiliki nilai 0. Kita ingin animasi dimulai dari -height
hingga 0
(posisi istirahat terakhirnya) sehingga dimulai dari atas dan bergerak masuk.
Saat menggunakan slideInVertically
, offset target untuk setelah bergeser masuk selalu 0
(piksel). initialOffsetY
dapat ditentukan sebagai nilai absolut atau persentase tinggi penuh elemen melalui fungsi lambda.
Demikian pula, slideOutVertically
mengasumsikan offset awal adalah 0, sehingga hanya targetOffsetY
yang perlu ditentukan.
AnimatedVisibility(
visible = shown,
enter = slideInVertically(
// Enters by sliding down from offset -fullHeight to 0.
initialOffsetY = { fullHeight -> -fullHeight }
),
exit = slideOutVertically(
// Exits by sliding up from offset 0 to -fullHeight.
targetOffsetY = { fullHeight -> -fullHeight }
)
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Dengan menjalankan kembali aplikasi, kita dapat melihat bahwa animasi lebih sesuai dengan yang kita harapkan:
Kita dapat menyesuaikan animasi lebih banyak dengan parameter animationSpec
. animationSpec
adalah parameter umum untuk banyak Animation API, termasuk EnterTransition
dan ExitTransition
. Kita dapat meneruskan salah satu dari berbagai jenis AnimationSpec
untuk menentukan bagaimana nilai animasi harus berubah dari waktu ke waktu. Dalam contoh ini, mari kita gunakan AnimationSpec
berbasis durasi yang sederhana. Instance ini dapat dibuat dengan fungsi tween
. Durasinya adalah 150 md, dan easing-nya adalah LinearOutSlowInEasing
. Untuk animasi keluar, mari kita gunakan fungsi tween
yang sama untuk parameter animationSpec
, tetapi dengan durasi 250 md dan easing FastOutLinearInEasing
.
Kode yang dihasilkan akan terlihat seperti berikut:
AnimatedVisibility(
visible = shown,
enter = slideInVertically(
// Enters by sliding down from offset -fullHeight to 0.
initialOffsetY = { fullHeight -> -fullHeight },
animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)
),
exit = slideOutVertically(
// Exits by sliding up from offset 0 to -fullHeight.
targetOffsetY = { fullHeight -> -fullHeight },
animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing)
)
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Jalankan aplikasi dan klik FAB lagi. Anda dapat melihat bahwa pesan kini bergeser masuk dan keluar dari atas dengan berbagai fungsi dan durasi easing:
5. Menganimasikan perubahan ukuran konten
Aplikasi menampilkan beberapa topik dalam konten. Coba klik salah satunya, dan teks akan terbuka serta menampilkan teks isi untuk topik tersebut. Kartu yang berisi teks akan meluas dan menyusut saat isi ditampilkan atau disembunyikan.
Lihat kode untuk TODO 3 di composable TopicRow
.
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// ... the title and the body
}
Composable Column
di sini mengubah ukurannya saat kontennya diubah. Kita dapat menganimasikan perubahan ukurannya dengan menambahkan pengubah animateContentSize
.
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.animateContentSize()
) {
// ... the title and the body
}
Jalankan aplikasi dan klik salah satu topik. Anda dapat melihat elemen ini meluas dan menciut dengan animasi.
animateContentSize
juga dapat disesuaikan dengan animationSpec
khusus. Kita dapat memberikan opsi untuk mengubah jenis animasi dari spring ke tween, dll. Lihat dokumentasi Menyesuaikan Animasi untuk informasi selengkapnya.
6. Menganimasikan beberapa nilai
Setelah memahami beberapa Animation API dasar, mari kita lihat Transition
API yang memungkinkan kita membuat animasi yang lebih kompleks. Menggunakan Transition
API memungkinkan kita melacak saat semua animasi di Transition
selesai, yang tidak mungkin dilakukan jika menggunakan animate*AsState
API individual yang telah kita lihat sebelumnya. Transition
API juga memungkinkan kita untuk menentukan transitionSpec
yang berbeda saat bertransisi di antara berbagai status. Mari kita pelajari cara menggunakannya:
Untuk contoh ini, kita menyesuaikan indikator tab. Ini adalah persegi panjang yang ditampilkan di tab yang saat ini dipilih.
Temukan TODO 4 di composable HomeTabIndicator
, dan lihat bagaimana indikator tab diterapkan.
val indicatorLeft = tabPositions[tabPage.ordinal].left
val indicatorRight = tabPositions[tabPage.ordinal].right
val color = if (tabPage == TabPage.Home) PaleDogwood else Green
Di sini, indicatorLeft
adalah posisi horizontal tepi kiri indikator dalam baris tab. indicatorRight
adalah posisi horizontal tepi kanan indikator. Warnanya juga berubah antara jingga pucat dan hijau.
Untuk menganimasikan beberapa nilai ini secara bersamaan, kita dapat menggunakan Transition
. Transition
dapat dibuat dengan fungsi updateTransition
. Teruskan indeks tab yang saat ini dipilih sebagai parameter targetState
.
Setiap nilai animasi dapat dideklarasikan dengan fungsi ekstensi animate*
dari Transition
. Dalam contoh ini, kita menggunakan animateDp
dan animateColor
. Fungsi ini mengambil blok lambda dan kita dapat menentukan nilai target untuk setiap status. Kita sudah mengetahui nilai targetnya sehingga kita dapat menggabungkan nilai seperti di bawah. Perhatikan bahwa kita dapat menggunakan deklarasi by
dan menjadikannya properti yang didelegasikan lokal di sini lagi karena fungsi animate*
menampilkan objek State
.
val transition = updateTransition(tabPage, label = "Tab indicator")
val indicatorLeft by transition.animateDp(label = "Indicator left") { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(label = "Indicator right") { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor(label = "Border color") { page ->
if (page == TabPage.Home) PaleDogwood else Green
}
Jalankan aplikasi sekarang dan Anda dapat melihat bahwa pengalihan tab sekarang jauh lebih menarik. Saat mengklik tab akan mengubah nilai status tabPage
, semua nilai animasi yang terkait dengan transition
mulai menganimasikan ke nilai yang ditentukan untuk status target.
Selain itu, kita dapat menentukan parameter transitionSpec
untuk menyesuaikan perilaku animasi. Misalnya, kita dapat memperoleh efek elastis untuk indikator ini dengan membuat tepi yang lebih dekat ke tujuan bergerak lebih cepat dari tepi lainnya. Kita dapat menggunakan fungsi infix isTransitioningTo
di lambda transitionSpec
untuk menentukan arah perubahan status.
val transition = updateTransition(
tabPage,
label = "Tab indicator"
)
val indicatorLeft by transition.animateDp(
transitionSpec = {
if (TabPage.Home isTransitioningTo TabPage.Work) {
// Indicator moves to the right.
// The left edge moves slower than the right edge.
spring(stiffness = Spring.StiffnessVeryLow)
} else {
// Indicator moves to the left.
// The left edge moves faster than the right edge.
spring(stiffness = Spring.StiffnessMedium)
}
},
label = "Indicator left"
) { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(
transitionSpec = {
if (TabPage.Home isTransitioningTo TabPage.Work) {
// Indicator moves to the right
// The right edge moves faster than the left edge.
spring(stiffness = Spring.StiffnessMedium)
} else {
// Indicator moves to the left.
// The right edge moves slower than the left edge.
spring(stiffness = Spring.StiffnessVeryLow)
}
},
label = "Indicator right"
) { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor(
label = "Border color"
) { page ->
if (page == TabPage.Home) PaleDogwood else Green
}
Jalankan kembali aplikasi dan coba beralih tab.
Android Studio mendukung pemeriksaan Transisi di Pratinjau Compose. Untuk menggunakan Pratinjau Animasi, mulai mode interaktif dengan mengklik ikon "Mulai Pratinjau Animasi" di pojok kanan atas Composable dalam pratinjau (ikon ). Coba klik ikon untuk composable PreviewHomeTabBar
. Tindakan ini akan membuka panel "Animasi" baru.
Anda dapat menjalankan animasi dengan mengklik tombol ikon "Putar". Anda juga dapat menarik seekbar untuk melihat setiap frame animasi. Untuk deskripsi nilai animasi yang lebih baik, Anda dapat menentukan parameter label
di updateTransition
dan metode animate*
.
7. Animasi berulang
Coba klik tombol ikon muat ulang di samping suhu saat ini. Aplikasi mulai memuat informasi cuaca terbaru (aplikasi berpura-pura). Sebelum pemuatan selesai, Anda akan melihat indikator pemuatan, yaitu lingkaran dan kotak panjang berwarna abu-abu. Mari kita animasikan nilai alfa indikator ini untuk memperjelas bahwa proses tersebut masih berlangsung.
Temukan TODO 5 di composable LoadingRow
.
val alpha = 1f
Kita ingin membuat nilai ini bergerak antara 0f dan 1f berulang kali. Kita dapat menggunakan InfiniteTransition
untuk tujuan ini. API ini mirip dengan Transition
API di bagian sebelumnya. Keduanya menganimasikan beberapa nilai, tetapi meskipun Transition
menganimasikan nilai berdasarkan perubahan status, InfiniteTransition
menganimasikan nilai tanpa batas.
Untuk membuat InfiniteTransition
, gunakan fungsi rememberInfiniteTransition
. Kemudian, setiap perubahan nilai animasi dapat dideklarasikan dengan salah satu fungsi ekstensi animate*
dari InfiniteTransition
. Dalam hal ini, kita menganimasikan nilai alfa, jadi mari kita gunakan animatedFloat
. Parameter initialValue
harus 0f
, dan targetValue
1f
. Kita juga dapat menentukan AnimationSpec
untuk animasi ini, tetapi API ini hanya memerlukan InfiniteRepeatableSpec
. Gunakan fungsi infiniteRepeatable
untuk membuatnya. AnimationSpec
ini menggabungkan AnimationSpec
berbasis durasi dan membuatnya dapat diulang. Misalnya, kode yang dihasilkan akan terlihat seperti di bawah ini.
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = keyframes {
durationMillis = 1000
0.7f at 500
},
repeatMode = RepeatMode.Reverse
),
label = "alpha"
)
repeatMode
default adalah RepeatMode.Restart
. Transisi ini dari initialValue
ke targetValue
dan dimulai lagi pada initialValue
. Dengan menyetel repeatMode
ke RepeatMode.Reverse
, animasi akan berlangsung dari initialValue
ke targetValue
lalu dari targetValue
ke initialValue
. Animasi berlangsung dari 0 hingga 1 lalu 1 hingga 0.
Animasi keyFrames
adalah jenis animationSpec
lain (beberapa lainnya adalah tween
dan spring
) yang memungkinkan perubahan nilai yang sedang berlangsung pada milidetik yang berbeda. Kami awalnya menetapkan durationMillis
ke 1000 md. Kemudian kita dapat menentukan frame utama dalam animasi, misalnya, pada 500 md animasi, kita ingin nilai alfa menjadi 0,7 f. Hal ini akan mengubah progres animasi: animasi akan bergerak cepat dari 0 menjadi 0,7 dalam 500 md animasi, dan dari 0,7 menjadi 1,0 dari 500 md ke 1000 md animasi, melambat mendekati akhir.
Jika menginginkan lebih dari satu keyframe, kita dapat menentukan beberapa keyFrames
sebagai berikut:
animation = keyframes {
durationMillis = 1000
0.7f at 500
0.9f at 800
}
Jalankan aplikasi dan coba klik tombol refresh. Sekarang Anda dapat melihat animasi indikator pemuatan.
8. Animasi gestur
Di bagian terakhir ini, kita akan mempelajari cara menjalankan animasi berdasarkan input sentuh. Kita akan membuat pengubah swipeToDismiss
dari awal.
Temukan TODO 6-1 di pengubah swipeToDismiss
. Di sini, kita mencoba membuat pengubah yang membuat elemen dapat digeser dengan sentuhan. Saat elemen dilemparkan ke tepi layar, kita memanggil callback onDismissed
agar elemen dapat dihapus.
Untuk membuat pengubah swipeToDismiss
, ada beberapa konsep utama yang perlu kita pahami. Pertama, pengguna meletakkan jari mereka di layar, menghasilkan peristiwa sentuh dengan koordinat x dan y, lalu mereka akan memindahkan jari ke kanan atau ke kiri - memindahkan x dan y berdasarkan gerakan mereka. Item yang disentuh perlu dipindahkan dengan jari mereka, jadi kita akan memperbarui posisi item berdasarkan posisi dan kecepatan peristiwa sentuh.
Kita dapat menggunakan beberapa konsep yang dijelaskan dalam dokumentasi Gestur Compose. Dengan menggunakan pengubah pointerInput
, kita bisa mendapatkan akses level rendah ke peristiwa sentuh pointer yang masuk dan melacak kecepatan yang ditarik pengguna menggunakan pointer yang sama. Jika pengguna melepasnya sebelum item melewati batas untuk menutupnya, item tersebut akan dikembalikan ke posisinya.
Ada beberapa hal unik yang perlu dipertimbangkan dalam skenario ini. Pertama, setiap animasi yang sedang berjalan mungkin dicegat oleh peristiwa sentuh. Kedua, nilai animasi mungkin bukan satu-satunya sumber kebenaran. Dengan kata lain, kita mungkin perlu menyinkronkan nilai animasi dengan nilai yang berasal dari peristiwa sentuh.
Animatable
adalah API level terendah yang pernah kita lihat sejauh ini. Kode ini memiliki beberapa fitur yang berguna dalam skenario gestur, seperti kemampuan untuk langsung menangkap nilai baru yang berasal dari gestur dan menghentikan animasi yang sedang berlangsung saat peristiwa sentuh baru dipicu. Mari kita buat instance Animatable
dan menggunakannya untuk merepresentasikan offset horizontal elemen yang dapat digeser. Pastikan untuk mengimpor Animatable
dari androidx.compose.animation.core.Animatable
dan bukan androidx.compose.animation.Animatable
.
val offsetX = remember { Animatable(0f) } // Add this line
// used to receive user touch events
pointerInput {
// Used to calculate a settling position of a fling animation.
val decay = splineBasedDecay<Float>(this)
// Wrap in a coroutine scope to use suspend functions for touch events and animation.
coroutineScope {
while (true) {
// ...
TODO 6-2 adalah tempat kita baru saja menerima peristiwa sentuhan. Kita harus mencegat animasi jika sedang berjalan. Tindakan ini dapat dilakukan dengan memanggil stop
di Animatable
. Perhatikan bahwa panggilan akan diabaikan jika animasi tidak berjalan. VelocityTracker
akan digunakan untuk menghitung seberapa cepat pengguna berpindah dari kiri ke kanan. awaitPointerEventScope
adalah fungsi penangguhan yang dapat menunggu peristiwa input pengguna dan meresponsnya.
// Wait for a touch down event. Track the pointerId based on the touch
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // Add this line to cancel any on-going animations
// Prepare for drag events and record velocity of a fling gesture
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {
Pada TODO 6-3, kita terus menerima peristiwa tarik. Kita harus menyinkronkan posisi peristiwa sentuh ke dalam nilai animasi. Kita dapat menggunakan snapTo
di Animatable
untuk ini. snapTo
harus dipanggil di dalam blok launch
lain karena awaitPointerEventScope
dan horizontalDrag
adalah cakupan coroutine terbatas. Artinya, kode tersebut hanya dapat melakukan suspend
untuk awaitPointerEvents
, snapTo
bukan peristiwa pointer.
horizontalDrag(pointerId) { change ->
// Add these 4 lines
// Get the drag amount change to offset the item with
val horizontalDragOffset = offsetX.value + change.positionChange().x
// Need to call this in a launch block in order to run it separately outside of the awaitPointerEventScope
launch {
// Instantly set the Animable to the dragOffset to ensure its moving
// as the user's finger moves
offsetX.snapTo(horizontalDragOffset)
}
// Record the velocity of the drag.
velocityTracker.addPosition(change.uptimeMillis, change.position)
// Consume the gesture event, not passed to external
if (change.positionChange() != Offset.Zero) change.consume()
}
TODO 6-4 adalah tempat elemen baru saja dirilis dan dilempar. Kita perlu menghitung posisi akhir yang dilemparkan ke fling untuk memutuskan apakah kita harus menggeser elemen kembali ke posisi semula, atau menggesernya dan memanggil callback. Kita menggunakan objek decay
yang dibuat sebelumnya untuk menghitung targetOffsetX
:
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Add this line to calculate where it would end up with
// the current velocity and position
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
Pada TODO 6-5, kita akan memulai animasi. Namun sebelum itu, kita ingin menyetel batas nilai atas dan bawah ke Animatable
agar dapat berhenti begitu mencapai batas (-size.width
dan size.width
karena kita tidak ingin offsetX
dapat diperluas hingga mencapai dua nilai ini). Pengubah pointerInput
memungkinkan kita mengakses ukuran elemen dengan properti size
, jadi mari gunakan untuk mendapatkan batas.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
TODO 6-6 adalah tempat kita akhirnya dapat memulai animasi. Pertama-tama, kita membandingkan posisi penyelesaian fling yang kita hitung sebelumnya dan ukuran elemen. Jika posisi penyelesaian lebih kecil dari itu, berarti kecepatan fling tidak cukup. Kita dapat menggunakan animateTo
untuk menganimasikan nilai kembali ke 0f. Jika tidak, kita akan menggunakan animateDecay
untuk memulai animasi fling. Jika animasi selesai (kemungkinan besar dengan batas yang telah ditetapkan sebelumnya), kita dapat memanggil callback.
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back.
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// Enough velocity to slide away the element to the edge.
offsetX.animateDecay(velocity, decay)
// The element was swiped away.
onDismissed()
}
}
Terakhir, lihat TODO 6-7. Kita telah menyiapkan semua animasi dan gestur, jadi jangan lupa menerapkan offset ke elemen, hal ini akan memindahkan elemen di layar ke nilai yang dihasilkan oleh gestur atau animasi kita:
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
Sebagai hasil dari bagian ini, Anda akan mendapatkan kode seperti di bawah ini:
private fun Modifier.swipeToDismiss(
onDismissed: () -> Unit
): Modifier = composed {
// This Animatable stores the horizontal offset for the element.
val offsetX = remember { Animatable(0f) }
pointerInput(Unit) {
// Used to calculate a settling position of a fling animation.
val decay = splineBasedDecay<Float>(this)
// Wrap in a coroutine scope to use suspend functions for touch events and animation.
coroutineScope {
while (true) {
// Wait for a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
// Interrupt any ongoing animation.
offsetX.stop()
// Prepare for drag events and record velocity of a fling.
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {
horizontalDrag(pointerId) { change ->
// Record the position after offset
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
// Overwrite the Animatable value while the element is dragged.
offsetX.snapTo(horizontalDragOffset)
}
// Record the velocity of the drag.
velocityTracker.addPosition(change.uptimeMillis, change.position)
// Consume the gesture event, not passed to external
change.consumePositionChange()
}
}
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Calculate where the element eventually settles after the fling animation.
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
// The animation should end as soon as it reaches these bounds.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
launch {
if (targetOffsetX.absoluteValue <= size.width) {
// Not enough velocity; Slide back to the default position.
offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
} else {
// Enough velocity to slide away the element to the edge.
offsetX.animateDecay(velocity, decay)
// The element was swiped away.
onDismissed()
}
}
}
}
}
// Apply the horizontal offset to the element.
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
}
Jalankan aplikasi dan coba geser salah satu item tugas. Anda dapat melihat bahwa elemen bergeser kembali ke posisi default atau bergeser menjauh dan dihapus, bergantung pada kecepatan fling Anda. Anda juga dapat menangkap elemen tersebut saat menganimasikan.
9. Selamat!
Selamat! Anda telah mempelajari Compose Animation API dasar.
Dalam codelab ini, kita telah mempelajari cara menggunakan:
Animation API tingkat tinggi:
animatedContentSize
AnimatedVisibility
Animation API tingkat rendah:
animate*AsState
untuk menganimasikan satu nilaiupdateTransition
untuk menganimasikan beberapa nilaiinfiniteTransition
untuk menganimasikan nilai tanpa batasAnimatable
untuk membuat animasi kustom dengan gestur sentuh
Apa selanjutnya?
Lihat codelab lainnya di jalur Compose.
Untuk mempelajari lebih lanjut, lihat Animasi Compose dan dokumen referensi ini: