Geser untuk menutup atau memperbarui

Komponen SwipeToDismissBox memungkinkan pengguna menutup atau memperbarui item dengan menggesernya ke kiri atau kanan.

Platform API

Gunakan composable SwipeToDismissBox untuk menerapkan tindakan yang dipicu oleh gestur geser. Parameter utama mencakup:

  • state: Status SwipeToDismissBoxState yang dibuat untuk menyimpan nilai yang dihasilkan oleh penghitungan pada item geser, yang memicu peristiwa saat dihasilkan.
  • backgroundContent: Composable yang dapat disesuaikan yang ditampilkan di belakang konten item yang ditampilkan saat konten digeser.

Contoh dasar: Memperbarui atau menutup saat digeser

Cuplikan dalam contoh ini menunjukkan implementasi geser yang memperbarui item saat digeser dari awal ke akhir, atau menutup item saat digeser dari akhir ke awal.

data class TodoItem(
    val itemDescription: String,
    var isItemDone: Boolean = false
)

@Composable
fun TodoListItem(
    todoItem: TodoItem,
    onToggleDone: (TodoItem) -> Unit,
    onRemove: (TodoItem) -> Unit,
    modifier: Modifier = Modifier,
) {
    val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
        confirmValueChange = {
            if (it == StartToEnd) onToggleDone(todoItem)
            else if (it == EndToStart) onRemove(todoItem)
            // Reset item when toggling done status
            it != StartToEnd
        }
    )

    SwipeToDismissBox(
        state = swipeToDismissBoxState,
        modifier = modifier.fillMaxSize(),
        backgroundContent = {
            when (swipeToDismissBoxState.dismissDirection) {
                StartToEnd -> {
                    Icon(
                        painter = rememberVectorPainter(if (todoItem.isItemDone) AppIcons.CheckBox else AppIcons.CheckBoxOutlineBlank),
                        contentDescription = if (todoItem.isItemDone) "Done" else "Not done",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(Color.Blue)
                            .wrapContentSize(Alignment.CenterStart)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                EndToStart -> {
                    Icon(
                        painter = rememberVectorPainter(AppIcons.Delete),
                        contentDescription = "Remove item",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(Color.Red)
                            .wrapContentSize(Alignment.CenterEnd)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                Settled -> {}
            }
        }
    ) {
        ListItem(
            headlineContent = { Text(todoItem.itemDescription) },
            supportingContent = { Text("swipe me to update or remove.") }
        )
    }
}

Poin penting tentang kode

  • swipeToDismissBoxState mengelola status komponen. Callback ini akan memicu callback confirmValueChange setelah interaksi dengan item selesai. Isi callback menangani berbagai kemungkinan tindakan. Callback menampilkan boolean yang memberi tahu komponen apakah harus menampilkan animasi tutup. Dalam hal ini:
    • Jika item digeser dari awal hingga akhir, item tersebut akan memanggil lambda onToggleDone, yang meneruskan todoItem saat ini. Hal ini sesuai dengan memperbarui item daftar tugas.
    • Jika item digeser dari akhir ke awal, item tersebut akan memanggil lambda onRemove, dengan meneruskan todoItem saat ini. Tindakan ini sesuai dengan penghapusan item tugas.
    • it != StartToEnd: Baris ini menampilkan true jika arah geser bukan StartToEnd, dan false jika sebaliknya. Menampilkan false akan mencegah SwipeToDismissBox langsung menghilang setelah geser "tombol selesai", sehingga memungkinkan konfirmasi visual atau animasi.
  • SwipeToDismissBox memungkinkan interaksi geser horizontal pada setiap item. Saat tidak digunakan, komponen ini menampilkan konten bagian dalam, tetapi saat pengguna mulai menggeser, konten akan dipindahkan dan backgroundContent akan muncul. Konten normal dan backgroundContent mendapatkan batasan penuh dari penampung induk untuk merender dirinya sendiri. content digambar di atas backgroundContent. Dalam hal ini:
    • backgroundContent diimplementasikan sebagai Icon dengan warna latar belakang berdasarkan SwipeToDismissBoxValue:
    • Blue saat menggeser StartToEnd — mengalihkan item tugas.
    • Red saat menggeser EndToStart — menghapus item daftar tugas.
    • Tidak ada yang ditampilkan di latar belakang untuk Settled — saat item tidak digeser, tidak ada yang ditampilkan di latar belakang.
    • Demikian pula, Icon yang ditampilkan akan menyesuaikan dengan arah geser:
    • StartToEnd menampilkan ikon CheckBox saat item daftar tugas selesai dan ikon CheckBoxOutlineBlank saat belum selesai.
    • EndToStart menampilkan ikon Delete.

@Composable
fun SwipeItemExample() {
    val todoItems = remember {
        mutableStateListOf(
            TodoItem("Pay bills"), TodoItem("Buy groceries"),
            TodoItem("Go to gym"), TodoItem("Get dinner")
        )
    }

    LazyColumn {
        items(
            items = todoItems,
            key = { it.itemDescription }
        ) { todoItem ->
            TodoListItem(
                todoItem = todoItem,
                onToggleDone = { todoItem ->
                    todoItem.isItemDone = !todoItem.isItemDone
                },
                onRemove = { todoItem ->
                    todoItems -= todoItem
                },
                modifier = Modifier.animateItem()
            )
        }
    }
}

Poin penting tentang kode

  • mutableStateListOf(...) membuat daftar yang dapat diamati yang dapat menyimpan objek TodoItem. Saat item ditambahkan atau dihapus dari daftar ini, Compose akan merekomposisi bagian UI yang bergantung padanya.
    • Di dalam mutableStateListOf(), empat objek TodoItem diinisialisasi dengan deskripsinya masing-masing: "Bayar tagihan", "Beli bahan makanan", "Pergi ke gym", dan "Makan malam".
  • LazyColumn menampilkan daftar scroll vertikal todoItems.
  • onToggleDone = { todoItem -> ... } adalah fungsi callback yang dipanggil dari dalam TodoListItem saat pengguna menandai objek sebagai selesai. Metode ini memperbarui properti isItemDone dari todoItem. Karena todoItems adalah mutableStateListOf, perubahan ini memicu rekomposisi, yang mengupdate UI.
  • onRemove = { todoItem -> ... } adalah fungsi callback yang dipicu saat pengguna menghapus item. Tindakan ini akan menghapus todoItem tertentu dari daftar todoItems. Hal ini juga menyebabkan rekomposisi, dan item akan dihapus dari daftar yang ditampilkan.
  • Pengubah animateItem diterapkan ke setiap TodoListItem sehingga placementSpec pengubah dipanggil saat item telah ditutup. Tindakan ini menganimasikan penghapusan item, serta pengurutan ulang item lain dalam daftar.

Hasil

Video berikut menunjukkan fungsi geser untuk menutup dasar dari cuplikan sebelumnya:

Gambar 1. Implementasi dasar geser untuk menutup yang dapat menandai item sebagai selesai dan menampilkan animasi tutup untuk item dalam daftar.

Lihat file sumber GitHub untuk mengetahui kode contoh lengkapnya.

Contoh lanjutan: Menganimasikan warna latar belakang saat menggeser

Cuplikan berikut menunjukkan cara menggabungkan nilai minimum posisi untuk menganimasikan warna latar belakang item saat digeser.

data class TodoItem(
    val itemDescription: String,
    var isItemDone: Boolean = false
)

@Composable
fun TodoListItemWithAnimation(
    todoItem: TodoItem,
    onToggleDone: (TodoItem) -> Unit,
    onRemove: (TodoItem) -> Unit,
    modifier: Modifier = Modifier,
) {
    val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
        confirmValueChange = {
            if (it == StartToEnd) onToggleDone(todoItem)
            else if (it == EndToStart) onRemove(todoItem)
            // Reset item when toggling done status
            it != StartToEnd
        }
    )

    SwipeToDismissBox(
        state = swipeToDismissBoxState,
        modifier = modifier.fillMaxSize(),
        backgroundContent = {
            when (swipeToDismissBoxState.dismissDirection) {
                StartToEnd -> {
                    Icon(
                        painter = rememberVectorPainter(if (todoItem.isItemDone) AppIcons.CheckBox else AppIcons.CheckBoxOutlineBlank),
                        contentDescription = if (todoItem.isItemDone) "Done" else "Not done",
                        modifier = Modifier
                            .fillMaxSize()
                            .drawBehind {
                                drawRect(lerp(Color.LightGray, Color.Blue, swipeToDismissBoxState.progress))
                            }
                            .wrapContentSize(Alignment.CenterStart)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                EndToStart -> {
                    Icon(
                        painter = rememberVectorPainter(AppIcons.Delete),
                        contentDescription = "Remove item",
                        modifier = Modifier
                            .fillMaxSize()
                            .background(lerp(Color.LightGray, Color.Red, swipeToDismissBoxState.progress))
                            .wrapContentSize(Alignment.CenterEnd)
                            .padding(12.dp),
                        tint = Color.White
                    )
                }
                Settled -> {}
            }
        }
    ) {
        OutlinedCard(shape = RectangleShape) {
            ListItem(
                headlineContent = { Text(todoItem.itemDescription) },
                supportingContent = { Text("swipe me to update or remove.") }
            )
        }
    }
}

Poin penting tentang kode

  • drawBehind menggambar langsung ke kanvas di belakang konten composable Icon.
    • drawRect() menggambar persegi panjang di kanvas dan mengisi seluruh batas cakupan gambar dengan Color yang ditentukan.
  • Saat menggeser, warna latar belakang item akan bertransisi dengan lancar menggunakan lerp.
    • Untuk geser dari StartToEnd, warna latar belakang berubah secara bertahap dari abu-abu muda menjadi biru.
    • Untuk geser dari EndToStart, warna latar belakang berubah secara bertahap dari abu-abu muda menjadi merah.
    • Jumlah transisi dari satu warna ke warna berikutnya ditentukan oleh swipeToDismissBoxState.progress.
  • OutlinedCard menambahkan pemisahan visual yang halus di antara item daftar.

@Composable
private fun SwipeItemWithAnimationExample() {
    val todoItems = remember {
        mutableStateListOf(
            TodoItem("Pay bills"), TodoItem("Buy groceries"),
            TodoItem("Go to gym"), TodoItem("Get dinner")
        )
    }

    LazyColumn {
        items(
            items = todoItems,
            key = { it.itemDescription }
        ) { todoItem ->
            TodoListItemWithAnimation(
                todoItem = todoItem,
                onToggleDone = { todoItem ->
                    todoItem.isItemDone = !todoItem.isItemDone
                },
                onRemove = { todoItem ->
                    todoItems -= todoItem
                },
                modifier = Modifier.animateItem()
            )
        }
    }
}

Poin penting tentang kode

  • Untuk poin-poin penting tentang kode ini, lihat Poin-poin penting dari bagian sebelumnya, yang menjelaskan cuplikan kode yang identik.

Hasil

Video berikut menunjukkan fungsi lanjutan dengan warna latar belakang animasi:

Gambar 2. Implementasi geser untuk menampilkan atau menghapus, dengan warna latar belakang animasi dan nilai minimum yang lebih lama sebelum tindakan didaftarkan.

Lihat file sumber GitHub untuk mengetahui kode contoh lengkapnya.

Referensi lainnya