滑动即可关闭或更新

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 之上。在这种情况下:
    • backgroundContent 实现为 Icon,其背景颜色基于 SwipeToDismissBoxValue:
    • Blue(滑动 StartToEnd 时)- 切换待办项。
    • Red(滑动 EndToStart 时)- 删除待办事项。
    • Settled 的背景中不显示任何内容 - 当列表项未被滑动时,背景中不显示任何内容。
    • 同样,显示的 Icon 会根据滑动方向进行调整:
    • StartToEnd 会在待办事项已完成时显示 CheckBox 图标,在待办事项未完成时显示 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 对象使用各自的说明进行初始化:“Pay bills”(支付账单)、“Buy groceries”(购买杂货)、“Go to gym”(去健身房)和“Get dinner”(吃晚餐)。
  • LazyColumn 会显示一个垂直滚动的 todoItems 列表。
  • onToggleDone = { todoItem -> ... } 是一个回调函数,当用户将对象标记为“完成”时,系统会从 TodoListItem 中调用该函数。它会更新 todoItem 的 isItemDone 属性。由于 todoItems 是 mutableStateListOf,因此这项更改会触发重组,从而更新界面。
  • onRemove = { todoItem -> ... } 是在用户移除商品时触发的回调函数。它会从 todoItems 列表中移除特定的 todoItem。这也会导致重组,并且该项将从显示的列表中移除。
  • animateItem 修饰符会应用于每个 TodoListItem,以便在内容被关闭时调用修饰符的 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 源文件。

其他资源