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 la droite.

Surface de l'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 générée par les calculs sur l'élément de balayage, qui déclenche des événements lorsqu'il est généré.
  • backgroundContent: composable personnalisable affiché derrière le contenu de l'élément, qui s'affiche lorsque le contenu est balayé.

Exemple de base: Mise à jour ou refus en balayant l'écran

Les extraits de code de cet exemple montrent une implémentation de balayage qui met à jour l'élément lorsqu'il est balayé de gauche à droite ou le ferme lorsqu'il est balayé de droite à gauche.

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

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é de gauche à droite, il appelle le lambda onToggleDone en transmettant le todoItem actuel. Cela correspond à la mise à jour de la tâche.
    • Si l'élément est balayé de la fin au début, il appelle le lambda onRemove, en transmettant le 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. Le retour de false empêche la SwipeToDismissBox de disparaître immédiatement après un balayage "Activer/Désactiver", ce qui permet une confirmation ou une animation visuelle.
  • SwipeToDismissBox permet les interactions de balayage horizontal sur chaque élément. En mode veille, il affiche le contenu interne du composant, mais lorsqu'un utilisateur commence à balayer l'écran, le contenu est déplacé et backgroundContent s'affiche. Le contenu normal et le backgroundContent obtiennent toutes les contraintes du conteneur parent pour s'afficher. Le content est dessiné au-dessus du 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 du balayage StartToEnd : activation/désactivation d'un élément de la liste de tâches.
    • Red lorsque vous balayez l'écran EndToStart : suppression d'un élément de la liste de tâches.
    • Rien n'est affiché en arrière-plan pour Settled. Lorsque l'élément n'est pas balayé, rien n'est affiché en arrière-plan.
    • De même, le Icon affiché 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
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()
            )
        }
    }
}

Points clés concernant le code

  • mutableStateListOf(...) crée une liste observable pouvant contenir des objets TodoItem. Lorsqu'un élément est ajouté ou supprimé de cette liste, 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", "Acheter des produits alimentaires", "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é. Il met à jour la propriété isItemDone de todoItem. Étant donné que 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. Il supprime l'todoItem spécifique de la liste todoItems. Une recomposition est également effectuée, 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 des extraits précédents:

Figure 1. Implémentation de base du balayage pour ignorer qui peut à la fois marquer un élément comme terminé et afficher une animation d'invalidation pour un élément d'une liste.

Pour obtenir l'exemple de code complet, consultez le fichier source GitHub.

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

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 l'ensemble des limites du champ d'application du dessin avec la Color spécifiée.
  • Lorsque vous balayez l'écran, la couleur d'arrière-plan de l'élément passe en douceur à l'aide de lerp.
    • Pour un balayage à partir de StartToEnd, la couleur d'arrière-plan passe progressivement du gris clair au bleu.
    • Pour un balayage à partir de 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 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 en savoir plus sur ce code, consultez les points clés d'une section précédente, qui décrit un extrait de code identique.

Résultat

La vidéo suivante présente les fonctionnalités avancées 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.

Pour obtenir l'exemple de code complet, consultez le fichier source GitHub.

Ressources supplémentaires