Vuốt để đóng hoặc cập nhật

Thành phần SwipeToDismissBox cho phép người dùng đóng hoặc cập nhật một mục bằng cách vuốt mục đó sang trái hoặc phải.

Nền tảng API

Sử dụng thành phần kết hợp SwipeToDismissBox để triển khai các thao tác được kích hoạt bằng cử chỉ vuốt. Các tham số chính bao gồm:

  • state: Trạng thái SwipeToDismissBoxState được tạo để lưu trữ giá trị do các phép tính trên mục vuốt tạo ra, từ đó kích hoạt các sự kiện khi được tạo.
  • backgroundContent: Một thành phần kết hợp có thể tuỳ chỉnh hiển thị phía sau nội dung mục được hiển thị khi nội dung được vuốt.

Ví dụ cơ bản: Cập nhật hoặc đóng khi vuốt

Các đoạn mã trong ví dụ này cho thấy cách triển khai thao tác vuốt để cập nhật mục khi vuốt từ đầu đến cuối hoặc đóng mục khi vuốt từ cuối đến đầu.

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(
                        if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.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(
                        imageVector = Icons.Default.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.") }
        )
    }
}

Các điểm chính về mã

  • swipeToDismissBoxState quản lý trạng thái thành phần. Phương thức này sẽ kích hoạt lệnh gọi lại confirmValueChange sau khi tương tác với mục đó. Phần nội dung lệnh gọi lại xử lý các thao tác có thể thực hiện. Lệnh gọi lại trả về một boolean cho thành phần biết liệu thành phần đó có hiển thị ảnh động đóng hay không. Trong trường hợp này:
    • Nếu bạn vuốt mục từ đầu đến cuối, thì thao tác này sẽ gọi lambda onToggleDone, truyền todoItem hiện tại. Thao tác này tương ứng với việc cập nhật mục cần làm.
    • Nếu bạn vuốt mục từ cuối đến đầu, thì thao tác này sẽ gọi hàm lambda onRemove, truyền todoItem hiện tại. Điều này tương ứng với việc xoá mục cần làm.
    • it != StartToEnd: Dòng này trả về true nếu hướng vuốt không phải là StartToEndfalse nếu không. Việc trả về false sẽ ngăn SwipeToDismissBox biến mất ngay sau khi vuốt "đã bật/tắt", cho phép xác nhận bằng hình ảnh hoặc ảnh động.
  • SwipeToDismissBox cho phép tương tác vuốt theo chiều ngang trên từng mục. Ở trạng thái nghỉ, thành phần này hiển thị nội dung bên trong của thành phần, nhưng khi người dùng bắt đầu vuốt, nội dung sẽ được di chuyển ra xa và backgroundContent sẽ xuất hiện. Cả nội dung thông thường và backgroundContent đều nhận được các quy tắc ràng buộc đầy đủ của vùng chứa mẹ để hiển thị chính nội dung đó. content được vẽ trên backgroundContent. Trong trường hợp này:
    • backgroundContent được triển khai dưới dạng Icon có màu nền dựa trên SwipeToDismissBoxValue:
    • Blue khi vuốt StartToEnd – bật/tắt một việc cần làm.
    • Red khi vuốt EndToStart – xoá một việc cần làm.
    • Không có gì hiển thị ở chế độ nền cho Settled – khi mục không được vuốt, không có gì hiển thị ở chế độ nền.
    • Tương tự, Icon hiển thị sẽ điều chỉnh theo hướng vuốt:
    • StartToEnd hiển thị biểu tượng CheckBox khi việc cần làm đã hoàn tất và biểu tượng CheckBoxOutlineBlank khi việc cần làm chưa hoàn tất.
    • EndToStart hiển thị biểu tượng Delete.

@Composable
private 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()
            )
        }
    }
}

Các điểm chính về mã

  • mutableStateListOf(...) tạo một danh sách có thể quan sát có thể chứa các đối tượng TodoItem. Khi một mục được thêm hoặc xoá khỏi danh sách này, Compose sẽ kết hợp lại các phần của giao diện người dùng phụ thuộc vào mục đó.
    • Bên trong mutableStateListOf(), 4 đối tượng TodoItem được khởi tạo với nội dung mô tả tương ứng: "Thanh toán hoá đơn", "Mua đồ ăn", "Đi tập thể dục" và "Ăn tối".
  • LazyColumn hiển thị danh sách todoItems cuộn theo chiều dọc.
  • onToggleDone = { todoItem -> ... } là một hàm gọi lại được gọi từ trong TodoListItem khi người dùng đánh dấu một đối tượng là đã hoàn tất. Phương thức này cập nhật thuộc tính isItemDone của todoItem. Vì todoItems là một mutableStateListOf, nên thay đổi này sẽ kích hoạt quá trình kết hợp lại, cập nhật giao diện người dùng.
  • onRemove = { todoItem -> ... } là một hàm gọi lại được kích hoạt khi người dùng xoá mục. Thao tác này sẽ xoá todoItem cụ thể khỏi danh sách todoItems. Điều này cũng gây ra quá trình kết hợp lại và mục sẽ bị xoá khỏi danh sách hiển thị.
  • Đối tượng sửa đổi animateItem được áp dụng cho mỗi TodoListItem để placementSpec của đối tượng sửa đổi được gọi khi mục đã bị loại bỏ. Thao tác này sẽ tạo ảnh động cho việc xoá mục, cũng như sắp xếp lại các mục khác trong danh sách.

Kết quả

Video sau đây minh hoạ chức năng vuốt để đóng cơ bản từ các đoạn mã trước:

Hình 1. Cách triển khai cơ bản của thao tác vuốt để đóng có thể vừa đánh dấu một mục là hoàn tất vừa hiển thị ảnh động đóng cho một mục trong danh sách.

Hãy xem tệp nguồn GitHub để biết toàn bộ mã mẫu.

Ví dụ nâng cao: Tạo ảnh động cho màu nền khi vuốt

Các đoạn mã sau đây cho biết cách kết hợp ngưỡng vị trí để tạo ảnh động cho màu nền của một mục khi vuốt.

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(
                        if (todoItem.isItemDone) Icons.Default.CheckBox else Icons.Default.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(
                        imageVector = Icons.Default.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.") }
            )
        }
    }
}

Các điểm chính về mã

  • drawBehind vẽ trực tiếp vào canvas phía sau nội dung của thành phần kết hợp Icon.
    • drawRect() vẽ một hình chữ nhật trên canvas và lấp đầy toàn bộ ranh giới của phạm vi vẽ bằng Color đã chỉ định.
  • Khi vuốt, màu nền của mục sẽ chuyển đổi suôn sẻ bằng cách sử dụng lerp.
    • Khi vuốt từ StartToEnd, màu nền sẽ thay đổi dần từ màu xám nhạt sang màu xanh dương.
    • Khi vuốt từ EndToStart, màu nền sẽ thay đổi dần từ màu xám nhạt sang màu đỏ.
    • Mức độ chuyển đổi từ màu này sang màu khác được xác định bằng swipeToDismissBoxState.progress.
  • OutlinedCard tạo ra một sự phân tách hình ảnh tinh tế giữa các mục trong danh sách.

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

Các điểm chính về mã

  • Để biết các điểm chính về mã này, hãy xem phần Các điểm chính trong phần trước. Phần này mô tả một đoạn mã giống hệt.

Kết quả

Video sau đây cho thấy chức năng nâng cao với màu nền động:

Hình 2. Cách triển khai thao tác vuốt để hiển thị hoặc xoá, với màu nền động và ngưỡng dài hơn trước khi đăng ký hành động.

Hãy xem tệp nguồn GitHub để biết toàn bộ mã mẫu.

Tài nguyên khác