スワイプして閉じるか更新する

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(
                        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 ラムダが呼び出され、現在の todoItem が渡されます。これは、ToDo アイテムの更新に対応しています。
    • アイテムが最後から先頭にスワイプされた場合、onRemove ラムダが呼び出され、現在の todoItem が渡されます。これは、ToDo アイテムの削除に対応しています。
    • it != StartToEnd: この行は、スワイプ方向が StartToEnd でない場合 true を返し、それ以外の場合は false を返します。false を返すと、「切り替えが完了しました」のスワイプ後に SwipeToDismissBox がすぐに消えなくなり、視覚的な確認やアニメーションが可能になります。
  • SwipeToDismissBox を使用すると、各アイテムを横方向にスワイプできます。休止状態では、コンポーネントの内部コンテンツが表示されますが、ユーザーがスワイプするとコンテンツが移動し、backgroundContent が表示されます。通常のコンテンツと backgroundContent の両方が、レンダリングする親コンテナの完全な制約を取得します。contentbackgroundContent の上に描画されます。この場合:
    • backgroundContent は、SwipeToDismissBoxValue に基づく背景色を持つ Icon として実装されます。
    • BlueStartToEnd をスワイプ)- タスクアイテムを切り替える。
    • RedEndToStart をスワイプ)- タスクアイテムを削除します。
    • 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 はそれに依存する UI の部分を再コンポーズします。
    • mutableStateListOf() 内で、4 つの TodoItem オブジェクトが、それぞれ「支払い」、「食料品の購入」、「ジムに行く」、「夕食をとる」という説明で初期化されます。
  • LazyColumn は、todoItems の縦方向にスクロールするリストを表示します。
  • onToggleDone = { todoItem -> ... } は、ユーザーがオブジェクトを完了済みとしてマークしたときに TodoListItem 内から呼び出されるコールバック関数です。todoItemisItemDone プロパティが更新されます。todoItemsmutableStateListOf であるため、この変更により再コンポーズがトリガーされ、UI が更新されます。
  • onRemove = { todoItem -> ... } は、ユーザーがアイテムを削除したときにトリガーされるコールバック関数です。todoItems リストから特定の todoItem が削除されます。これにより再コンポーズも行われ、表示されるリストからアイテムが削除されます。
  • TodoListItemanimateItem 修飾子が適用されるため、アイテムが閉じられたときに修飾子の 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 からスワイプすると、背景色がライトグレーから赤に徐々に変化します。
    • 1 つの色から次の色への遷移量は、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 ソースファイルをご覧ください。

参考情報