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 composable SwipeToDismissBox để triển khai các hành động đượ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, kích hoạt các sự kiện khi được tạo.
  • backgroundContent: Thành phần composable có thể tuỳ chỉnh, hiển thị phía sau nội dung mục và xuất hiện 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 một 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(
                        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.") }
        )
    }
}

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

  • swipeToDismissBoxState quản lý trạng thái của thành phần. Thao tác này sẽ kích hoạt lệnh gọi lại confirmValueChange sau khi người dùng tương tác xong với mục. Nội dung của lệnh gọi lại xử lý các hành động có thể thực hiện. Lệnh gọi lại trả về một giá trị boolean cho thành phần biết liệu có nên hiển thị ảnh động đóng hay không. Trong trường hợp này:
    • Nếu mục được vuốt từ đầu đến cuối, thì mục đó sẽ gọi lambda onToggleDone, truyền todoItem hiện tại. Điều này tương ứng với việc cập nhật mục việc cần làm.
    • Nếu mục được vuốt từ cuối đến đầu, thì mục đó sẽ gọi hàm lambda onRemove, truyền todoItem hiện tại. Thao tác này tương ứng với việc xoá mục việ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à StartToEnd và false nếu không phải. Việc trả về false sẽ ngăn SwipeToDismissBox biến mất ngay lập tức sau khi vuốt "bật/tắt xong", cho phép xác nhận hoặc tạo ảnh động trực quan.
  • SwipeToDismissBox cho phép tương tác vuốt ngang trên mỗi mục. Khi ở trạng thái nghỉ, thành phần này sẽ hiển thị nội dung bên trong, nhưng khi người dùng bắt đầu vuốt, nội dung sẽ được di chuyển đi 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ẹ để tự kết xuất. content được vẽ lên 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 mục việc cần làm.
    • Không có nội dung nào hiển thị trong nền cho Settled — khi mục không được vuốt, không có nội dung nào hiển thị trong nền.
    • Tương tự, Icon hiển thị sẽ điều chỉnh theo hướng vuốt:
    • StartToEnd cho thấy 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
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 vào hoặc bị 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(), bốn đối tượng TodoItem được khởi tạo bằng nội dung mô tả tương ứng: "Thanh toán hoá đơn", "Mua hàng tạp hoá", "Đi tập thể dục" và "Ăn tối".
  • LazyColumn hiển thị danh sách cuộn theo chiều dọc của todoItems.
  • onToggleDone = { todoItem -> ... } là một hàm callback được gọi từ trong TodoListItem khi người dùng đánh dấu một đối tượng là đã hoàn tất. Thành phần 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 callback đượ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. Thao tác này cũng gây ra quá trình kết hợp lại và mục này sẽ bị xoá khỏi danh sách hiển thị.
  • Đối tượng sửa đổi animateItem được áp dụng cho từng 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 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 trong 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 mã mẫu đầy đủ.

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(
                        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.") }
            )
        }
    }
}

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 được chỉ định.
  • Khi vuốt, màu nền của mục sẽ chuyển đổi mượt mà bằng cách sử dụng lerp.
    • Khi vuốt từ StartToEnd, màu nền sẽ dần chuyển từ màu xám nhạt sang màu xanh dương.
    • Khi vuốt từ EndToStart, màu nền sẽ dần chuyển từ màu xám nhạt sang màu đỏ.
    • Mức độ chuyển đổi từ màu này sang màu tiếp theo được xác định bằng swipeToDismissBoxState.progress.
  • OutlinedCard thêm một đường phân tách trực quan 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 Điểm chính trong phần trước, trong đó 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 hoặc xoá, với màu nền động và ngưỡng dài hơn trước khi thao tác được ghi nhận.

Hãy xem tệp nguồn GitHub để biết mã mẫu đầy đủ.

Tài nguyên khác