滑動即可關閉或更新

SwipeToDismissBox 元件可讓使用者向左或向右滑動,關閉或更新項目。

API 介面

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

程式碼重點

  • swipeToDismissBoxState 會管理元件狀態。與項目互動完成後,系統會觸發 confirmValueChange 回呼。回呼主體會處理各種可能的操作。回呼會傳回布林值,告知元件是否應顯示關閉動畫。具體如下:
    • 如果項目從頭滑動到尾,則會呼叫 onToggleDone lambda,並傳遞目前的 todoItem。這對應於更新待辦事項。
    • 如果項目從結尾滑動到開頭,則會呼叫 onRemove lambda,並傳遞目前的 todoItem。這相當於刪除待辦事項。
    • it != StartToEnd:如果滑動方向不是 StartToEnd,這一行會傳回 true,否則會傳回 false。傳回 false 可防止 SwipeToDismissBox 在「切換完成」滑動後立即消失,以便進行視覺確認或動畫。
  • SwipeToDismissBox 可在每個項目上啟用水平滑動互動。在靜止狀態下,它會顯示元件的內部內容,但當使用者開始滑動時,內容會移開,並顯示 backgroundContent。一般內容和 backgroundContent 都會取得父項容器的完整限制,以便在其中算繪。content 會繪製在 backgroundContent 之上。具體如下:
    • backgroundContent 是以 Icon 的形式實作,背景顏色則根據 SwipeToDismissBoxValue 決定:
    • 滑動 StartToEnd 時為 Blue,切換待辦事項。
    • 滑動 EndToStart 時,Red 會刪除待辦事項。
    • Settled 的背景不會顯示任何內容,也就是說,如果沒有滑動項目,背景就不會顯示任何內容。
    • 同樣地,顯示的 Icon 會根據滑動方向調整:
    • StartToEnd 會在待辦事項完成時顯示 CheckBox 圖示,未完成時則顯示 CheckBoxOutlineBlank 圖示。
    • EndToStart 會顯示 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()
            )
        }
    }
}

程式碼重點

  • mutableStateListOf(...) 會建立可觀察清單,用於存放 TodoItem 物件。當清單中新增或移除項目時,Compose 會重組依賴該項目的 UI 部分。
    • 在 mutableStateListOf() 中,四個 TodoItem 物件會使用各自的說明進行初始化:「Pay bills」、「Buy groceries」、「Go to gym」和「Get dinner」。
  • LazyColumn 會顯示 todoItems 的垂直捲動清單。
  • onToggleDone = { todoItem -> ... } 是在使用者將物件標示為完成時,從 TodoListItem 內叫用的回呼函式。它會更新 todoItem 的 isItemDone 屬性。由於 todoItems 是 mutableStateListOf,因此這項變更會觸發重組,更新 UI。
  • 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(
                        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.") }
            )
        }
    }
}

程式碼重點

  • 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 來源檔案。

其他資源