Scorri per ignorare o aggiornare

Il componente SwipeToDismissBox consente a un utente di ignorare o aggiornare un elemento scorrendo verso sinistra o verso destra.

Piattaforma API

Utilizza il componibile SwipeToDismissBox per implementare le azioni attivate dai gesti di scorrimento. I parametri principali includono:

  • state: lo stato SwipeToDismissBoxState creato per archiviare il valore prodotto dai calcoli sull'elemento di scorrimento, che attiva gli eventi quando viene prodotto.
  • backgroundContent: un componibile personalizzabile visualizzato dietro i contenuti dell'elemento che viene mostrato quando i contenuti vengono fatti scorrere.

Esempio di base: aggiornamento o chiusura con scorrimento

Gli snippet di questo esempio mostrano un'implementazione dello scorrimento che aggiorna l'elemento quando viene fatto scorrere dall'inizio alla fine o lo chiude quando viene fatto scorrere dalla fine all'inizio.

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

Punti chiave sul codice

  • swipeToDismissBoxState gestisce lo stato del componente. Attiva il callback confirmValueChange una volta completata l'interazione con l'elemento. Il corpo del callback gestisce le diverse azioni possibili. Il callback restituisce un valore booleano che indica al componente se deve visualizzare un'animazione di chiusura. In questo caso:
    • Se l'elemento viene fatto scorrere dall'inizio alla fine, chiama la funzione lambda onToggleDone, passando l'elemento todoItem corrente. Questo corrisponde all'aggiornamento dell'attività da svolgere.
    • Se l'elemento viene fatto scorrere dalla fine all'inizio, chiama la funzione lambda onRemove, passando l'elemento todoItem corrente. Questo corrisponde all'eliminazione dell'attività.
    • it != StartToEnd: questa riga restituisce true se la direzione di scorrimento non è StartToEnd, altrimenti restituisce false. La restituzione di false impedisce la scomparsa immediata di SwipeToDismissBox dopo uno scorrimento "Attiva/disattiva completato", consentendo una conferma visiva o un'animazione.
  • SwipeToDismissBox consente interazioni di scorrimento orizzontale su ogni elemento. In stato di riposo, mostra i contenuti interni del componente, ma quando un utente inizia a scorrere, i contenuti vengono spostati e viene visualizzato backgroundContent. Sia i contenuti normali sia il backgroundContent ricevono i vincoli completi del contenitore principale in cui eseguire il rendering. content viene disegnato sopra backgroundContent. In questo caso:
    • backgroundContent è implementato come Icon con un colore di sfondo basato su SwipeToDismissBoxValue:
    • Blue quando scorri StartToEnd: attiva/disattiva un'attività da svolgere.
    • Red quando scorri EndToStart: eliminazione di un'attività.
    • Non viene visualizzato nulla sullo sfondo per Settled: quando l'elemento non viene fatto scorrere, non viene visualizzato nulla sullo sfondo.
    • Analogamente, l'elemento Icon visualizzato si adatta alla direzione di scorrimento:
    • StartToEnd mostra un'icona CheckBox quando l'attività è completata e un'icona CheckBoxOutlineBlank quando non lo è.
    • EndToStart mostra un'icona 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()
            )
        }
    }
}

Punti chiave sul codice

  • mutableStateListOf(...) crea un elenco osservabile che può contenere oggetti TodoItem. Quando un elemento viene aggiunto o rimosso da questo elenco, Compose ricompone le parti della UI che dipendono da questo elemento.
    • All'interno di mutableStateListOf(), vengono inizializzati quattro oggetti TodoItem con le rispettive descrizioni: "Paga le bollette", "Fai la spesa", "Vai in palestra" e "Cena".
  • LazyColumn mostra un elenco a scorrimento verticale di todoItems.
  • onToggleDone = { todoItem -> ... } è una funzione di callback richiamata da TodoListItem quando l'utente contrassegna un oggetto come completato. Aggiorna la proprietà isItemDone di todoItem. Poiché todoItems è un mutableStateListOf, questa modifica attiva una ricomposizione, aggiornando la UI.
  • onRemove = { todoItem -> ... } è una funzione di callback attivata quando l'utente rimuove l'elemento. Rimuove l'elemento todoItem specifico dall'elenco todoItems. Inoltre, viene attivata una ricomposizione e l'elemento viene rimosso dall'elenco visualizzato.
  • A ogni TodoListItem viene applicato un modificatore animateItem in modo che venga chiamato il placementSpec del modificatore quando l'elemento viene ignorato. In questo modo, viene animata la rimozione dell'elemento e il riordinamento degli altri elementi nell'elenco.

Risultato

Il seguente video mostra la funzionalità di base di scorrimento per chiudere degli snippet precedenti:

Figura 1. Un'implementazione di base dello scorrimento per chiudere che può sia contrassegnare un elemento come completato sia mostrare un'animazione di chiusura per un elemento in un elenco.

Per il codice campione completo, consulta il file di codice sorgente GitHub.

Esempio avanzato: animazione del colore di sfondo allo scorrimento

Gli snippet seguenti mostrano come incorporare una soglia di posizione per animare il colore di sfondo di un elemento quando viene fatto scorrere.

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

Punti chiave sul codice

  • drawBehind disegna direttamente sul canvas dietro i contenuti del componibile Icon.
    • drawRect() disegna un rettangolo sul canvas e riempie l'intero perimetro dell'ambito di disegno con il colore Color specificato.
  • Durante lo scorrimento, il colore di sfondo dell'elemento cambia gradualmente utilizzando lerp.
    • Per uno scorrimento da StartToEnd, il colore dello sfondo cambia gradualmente da grigio chiaro a blu.
    • Per uno scorrimento da EndToStart, il colore dello sfondo cambia gradualmente da grigio chiaro a rosso.
    • La quantità di transizione da un colore all'altro è determinata da swipeToDismissBoxState.progress.
  • OutlinedCard aggiunge una sottile separazione visiva tra gli elementi dell'elenco.

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

Punti chiave sul codice

  • Per i punti chiave di questo codice, consulta la sezione Punti chiave di una sezione precedente, che descrive uno snippet di codice identico.

Risultato

Il seguente video mostra la funzionalità avanzata con il colore di sfondo animato:

Figura 2. Un'implementazione dello scorrimento per mostrare o eliminare, con colori di sfondo animati e una soglia più lunga prima della registrazione dell'azione.

Per il codice campione completo, consulta il file di codice sorgente GitHub.

Risorse aggiuntive