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

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 ラムダが呼び出され、現在の todoItem が渡されます。これは、ToDo アイテムの更新に対応します。
    • アイテムが末尾から先頭にスワイプされると、onRemove ラムダが呼び出され、現在の todoItem が渡されます。これは、ToDo アイテムの削除に対応します。
    • it != StartToEnd: この行は、スワイプの方向が StartToEnd でない場合は true を返し、それ以外の場合は false を返します。false を返すと、SwipeToDismissBox が「切り替え完了」スワイプの直後に消えるのを防ぎ、視覚的な確認やアニメーションが可能になります。
  • SwipeToDismissBox を使用すると、各アイテムで水平スワイプ操作が可能になります。静止状態では、コンポーネントの内部コンテンツが表示されますが、ユーザーがスワイプを開始すると、コンテンツが移動して backgroundContent が表示されます。通常のコンテンツと backgroundContent の両方が、親コンテナの完全な制約を取得して、自身をレンダリングします。content は backgroundContent の上に描画されます。この場合、次のようになります。
    • backgroundContent は、SwipeToDismissBoxValue に基づく背景色を持つ Icon として実装されます。
    • Blue(StartToEnd をスワイプしたとき)- ToDo アイテムを切り替えます。
    • Red(EndToStart をスワイプしたとき)- ToDo アイテムを削除します。
    • Settled の背景には何も表示されません。アイテムがスワイプされていない場合、背景には何も表示されません。
    • 同様に、表示される Icon はスワイプの方向に応じて変化します。
    • StartToEnd は、ToDo アイテムが完了している場合は 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() 内で、4 つの 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 ソースファイルをご覧ください。

参考情報