Menganimasikan elemen di Jetpack Compose

1. Pengantar

Logo Jetpack Compose

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

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.

Mengimpor Codelab Animasi ke 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.

Menampilkan start yang dipilih di Android Studio

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.

Daftar TODO yang ditampilkan di Android Studio

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.

Tab Rumah Dipilih

Tab Kerja Dipilih

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.

Animasi Perubahan Warna yang berfungsi di antara tab

4. Menganimasikan visibilitas

Jika Anda men-scroll konten aplikasi, Anda akan melihat tombol tindakan mengambang meluas dan menyusut, bergantung pada arah scroll Anda.

Tombol tindakan mengambang Edit diperluas

Tombol tindakan mengambang Edit kecil

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.

Animasi tombol edit tindakan mengambang

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.

Pesan yang menjelaskan bahwa fitur edit tidak didukung

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.

Geser keluar secara vertikal terpotong setengah

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:

Slide dalam animasi dengan offset berfungsi

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:

Animasi yang menampilkan pesan edit yang bergeser masuk dari atas

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.

Daftar topik yang diciutkan

Daftar topik diluaskan

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.

Animasi daftar topik yang diluaskan dan diciutkan

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.

Tab rumah dipilih

Tab kerja 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.

Animasi antara tab rumah dan kantor

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.

Efek elastis kustom pada pengalihan 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 Ikon pratinjau animasi). 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*.

Mencari animasi di Android Studio

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.

Gambar statis kartu info placeholder yang belum dianimasikan.

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.

Mengulang konten placeholder animasi

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.

Geser Animasi Gestur untuk menutup item

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 nilai
  • updateTransition untuk menganimasikan beberapa nilai
  • infiniteTransition untuk menganimasikan nilai tanpa batas
  • Animatable 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: