滑动即可关闭或更新

借助 SwipeToDismissBox 组件,用户可以通过向左或向右滑动项来关闭或更新项。

API Surface

使用 SwipeToDismissBox 可组合项实现由滑动手势触发的操作。关键参数包括:

  • state:创建的 SwipeToDismissBoxState 状态,用于存储对滑动项进行计算所产生的值,该值在生成时会触发事件。
  • backgroundContent:显示在项内容后面的可自定义可组合项,会在内容滑动时显示。

基本示例:滑动更新或关闭

此示例中的代码段展示了一种滑动实现,当从左向右滑动时,会更新项;当从右向左滑动时,会关闭项。

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

代码要点

  • swipeToDismissBoxState 用于管理组件状态。它会在与项完成互动后触发 confirmValueChange 回调。回调正文会处理可能执行的不同操作。回调会返回一个布尔值,用于告知组件是否应显示关闭动画。在这种情况下:
    • 如果从头滑动到尾,则会调用 onToggleDone lambda,并传递当前 todoItem。这与更新待办事项相对应。
    • 如果从右向左滑动该项,系统会调用 onRemove lambda,并传递当前 todoItem。这相当于删除待办事项。
    • it != StartToEnd:如果滑动方向不是 StartToEnd,则此行会返回 true;否则,会返回 false。返回 false 可防止 SwipeToDismissBox 在“切换完成”滑动后立即消失,从而允许进行视觉确认或动画。
  • SwipeToDismissBox 可让用户对每个项执行横向滑动互动。在静态状态下,它会显示组件的内部内容,但当用户开始滑动时,内容会移开,并显示 backgroundContent。正常内容和 backgroundContent 都会获取父容器的完整约束条件,以便在其中呈现自己。content 会绘制在 backgroundContent 之上。在这种情况下:
    • backgroundContentIcon 的形式实现,背景颜色基于 SwipeToDismissBoxValue
    • 滑动 StartToEndBlue - 切换待办事项。
    • 滑动 RedEndToStart - 删除待办事项。
    • Settled 的背景中没有显示任何内容 - 当用户未滑动该项时,背景中不会显示任何内容。
    • 同样,显示的 Icon 会根据滑动方向进行调整:
    • 当待办事项已完成时,StartToEnd 会显示 CheckBox 图标;当待办事项未完成时,StartToEnd 会显示 CheckBoxOutlineBlank 图标。
    • EndToStart 会显示 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()
            )
        }
    }
}

代码要点

  • mutableStateListOf(...) 会创建一个可包含 TodoItem 对象的可观察列表。当此列表中添加或移除项时,Compose 会重组依赖于该项的界面部分。
    • mutableStateListOf() 中,四个 TodoItem 对象会使用各自的说明进行初始化:“付账”“买杂货”“去健身房”和“吃晚餐”。
  • LazyColumn 会显示垂直滚动的 todoItems 列表。
  • onToggleDone = { todoItem -> ... } 是当用户将对象标记为已完成时从 TodoListItem 内调用的回调函数。它会更新 todoItemisItemDone 属性。由于 todoItemsmutableStateListOf,因此此更改会触发重组,从而更新界面。
  • onRemove = { todoItem -> ... } 是用户移除项时触发的回调函数。它会从 todoItems 列表中移除特定的 todoItem。这也会导致重新组合,并且相应项会从显示的列表中移除。
  • 系统会对每个 TodoListItem 应用 animateItem 修饰符,以便在项关闭时调用修饰符的 placementSpec。这会为移除项以及列表中其他项的重新排序添加动画效果。

结果

以下视频演示了上述代码段中的基本滑动关闭功能:

图 1。滑动关闭的基本实现,既可以将项标记为已完成,也可以为列表中的项显示关闭动画。

如需查看完整的示例代码,请参阅 GitHub 源文件

高级示例:在滑动时为背景颜色添加动画效果

以下代码段展示了如何采用位置阈值来为滑动操作添加项背景颜色动画。

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

代码要点

  • drawBehind 会直接绘制到 Icon 可组合项内容后面的画布中。
    • drawRect() 会在画布上绘制一个矩形,并使用指定的 Color 填充绘制范围的整个边界。
  • 滑动时,项的背景颜色会使用 lerp 平滑过渡。
    • 对于从 StartToEnd 滑动的情况,背景颜色会从浅灰色逐渐变为蓝色。
    • 如果从 EndToStart 滑动,背景颜色会从浅灰色逐渐变为红色。
    • 从一种颜色过渡到另一种颜色的量取决于 swipeToDismissBoxState.progress
  • OutlinedCard 会在列表项之间添加细微的外观分隔。

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

代码要点

  • 如需了解此代码的要点,请参阅上一节中的要点,其中介绍了与此代码段完全相同的代码段。

结果

以下视频展示了具有动画背景颜色的高级功能:

图 2。实现了滑动显示或删除功能,并采用了动画背景颜色,延长了操作注册前的阈值。

如需查看完整的示例代码,请参阅 GitHub 源文件

其他资源