Balayez l'écran pour ignorer ou mettre à jour

Le composant SwipeToDismissBox permet à un utilisateur d'ignorer ou de mettre à jour un élément en le balayant vers la gauche ou vers la droite.

Surface d'API

Utilisez le composable SwipeToDismissBox pour implémenter des actions déclenchées par des gestes de balayage. Voici les principaux paramètres :

  • state : état SwipeToDismissBoxState créé pour stocker la valeur produite par les calculs sur l'élément de balayage, qui déclenche des événements lorsqu'il est produit.
  • backgroundContent : composable personnalisable affiché derrière le contenu de l'élément, qui est révélé lorsque l'utilisateur balaie l'écran.

Exemple de base : mettre à jour ou ignorer en balayant l'écran

Les extraits de cet exemple montrent une implémentation de balayage qui met à jour l'élément lorsqu'il est balayé du début à la fin, ou qui le supprime lorsqu'il est balayé de la fin au début.

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

Points clés concernant le code

  • swipeToDismissBoxState gère l'état du composant. Il déclenche le rappel confirmValueChange une fois l'interaction avec l'élément terminée. Le corps du rappel gère les différentes actions possibles. Le rappel renvoie une valeur booléenne qui indique au composant s'il doit afficher une animation de fermeture. Dans ce cas :
    • Si l'élément est balayé du début à la fin, il appelle le lambda onToggleDone, en transmettant l'todoItem actuel. Cela correspond à la mise à jour de l'élément de la liste de tâches.
    • Si l'élément est balayé de la fin vers le début, il appelle le lambda onRemove en transmettant l'élément todoItem actuel. Cela correspond à la suppression de l'élément de la liste de tâches.
    • it != StartToEnd : cette ligne renvoie true si la direction du balayage n'est pas StartToEnd, et false dans le cas contraire. Renvoyer false empêche le SwipeToDismissBox de disparaître immédiatement après un balayage "marquer comme terminé", ce qui permet d'afficher une confirmation visuelle ou une animation.
  • SwipeToDismissBox permet d'activer les interactions de balayage horizontal sur chaque élément. Au repos, il affiche le contenu interne du composant, mais lorsqu'un utilisateur commence à balayer l'écran, le contenu est déplacé et le backgroundContent s'affiche. Le contenu normal et le backgroundContent obtiennent les contraintes complètes du conteneur parent pour s'afficher. Le content est dessiné au-dessus de la backgroundContent. Dans ce cas :
    • backgroundContent est implémenté en tant que Icon avec une couleur d'arrière-plan basée sur SwipeToDismissBoxValue :
    • Blue lors d'un balayage vers StartToEnd : cocher ou décocher une tâche à faire.
    • Red lors du balayage de EndToStart : suppression d'une tâche.
    • Rien ne s'affiche en arrière-plan pour Settled. Lorsque l'élément n'est pas balayé, rien ne s'affiche en arrière-plan.
    • De même, le Icon qui s'affiche s'adapte à la direction du balayage :
    • StartToEnd affiche une icône CheckBox lorsque la tâche est terminée et une icône CheckBoxOutlineBlank lorsqu'elle ne l'est pas.
    • EndToStart affiche une icône 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()
            )
        }
    }
}

Points clés concernant le code

  • mutableStateListOf(...) crée une liste observable pouvant contenir des objets TodoItem. Lorsqu'un élément est ajouté à cette liste ou en est supprimé, Compose recompose les parties de l'UI qui en dépendent.
    • Dans mutableStateListOf(), quatre objets TodoItem sont initialisés avec leurs descriptions respectives : "Payer les factures", "Faire les courses", "Aller à la salle de sport" et "Dîner".
  • LazyColumn affiche une liste à défilement vertical de todoItems.
  • onToggleDone = { todoItem -> ... } est une fonction de rappel appelée depuis TodoListItem lorsque l'utilisateur marque un objet comme terminé. Elle met à jour la propriété isItemDone de todoItem. Comme todoItems est un mutableStateListOf, cette modification déclenche une recomposition, ce qui met à jour l'UI.
  • onRemove = { todoItem -> ... } est une fonction de rappel déclenchée lorsque l'utilisateur supprime l'élément. Elle supprime l'élément todoItem spécifique de la liste todoItems. Cela entraîne également une recomposition, et l'élément est supprimé de la liste affichée.
  • Un modificateur animateItem est appliqué à chaque TodoListItem afin que le placementSpec du modificateur soit appelé lorsque l'élément a été ignoré. Cela anime la suppression de l'élément, ainsi que la réorganisation des autres éléments de la liste.

Résultat

La vidéo suivante illustre la fonctionnalité de base de balayage pour ignorer à partir des extraits précédents :

Figure 1. Implémentation de base de la fonctionnalité Balayer pour ignorer, qui permet à la fois de marquer un élément comme terminé et d'afficher une animation de suppression pour un élément d'une liste.

Consultez le fichier source GitHub pour obtenir l'exemple de code complet.

Exemple avancé : animer la couleur d'arrière-plan lors d'un balayage

Les extraits de code suivants montrent comment intégrer un seuil de position pour animer la couleur d'arrière-plan d'un élément lors d'un balayage.

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

Points clés concernant le code

  • drawBehind dessine directement dans le canevas derrière le contenu du composable Icon.
    • drawRect() dessine un rectangle sur le canevas et remplit toutes les limites du champ d'application du dessin avec le Color spécifié.
  • Lors du balayage, la couleur d'arrière-plan de l'élément change de manière fluide à l'aide de lerp.
    • Pour un balayage depuis StartToEnd, la couleur d'arrière-plan passe progressivement du gris clair au bleu.
    • Pour un balayage depuis EndToStart, la couleur d'arrière-plan passe progressivement du gris clair au rouge.
    • La quantité de transition d'une couleur à l'autre est déterminée par swipeToDismissBoxState.progress.
  • OutlinedCard ajoute une séparation visuelle subtile entre les éléments de la liste.

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

Points clés concernant le code

  • Pour connaître les points clés de ce code, consultez la section Points clés d'une section précédente, qui décrit un extrait de code identique.

Résultat

La vidéo suivante montre la fonctionnalité avancée avec une couleur d'arrière-plan animée :

Figure 2. Implémentation du balayage pour afficher ou supprimer, avec des couleurs d'arrière-plan animées et un seuil plus long avant l'enregistrement de l'action.

Consultez le fichier source GitHub pour obtenir l'exemple de code complet.

Ressources supplémentaires