Scorri per ignorare o aggiornare

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

API surface

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

  • state: lo stato SwipeToDismissBoxState creato per memorizzare il valore prodotto dai calcoli sull'elemento scorrimento, che attiva gli eventi quando viene prodotto.
  • backgroundContent: un componibile personalizzabile visualizzato dietro i contenuti dell'articolo che viene rivelato quando si scorre.

Esempio di base: aggiornamento o chiusura con scorrimento

Gli snippet in questo esempio mostrano un'implementazione dello scorrimento che aggiorna l'elemento quando viene eseguito dall'inizio alla fine o lo chiude quando viene eseguito 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 del codice

  • swipeToDismissBoxState gestisce lo stato del componente. Attiva il callback confirmValueChange al termine dell'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 mostrare un'animazione di chiusura. In questo caso:
    • Se l'elemento viene strisciato dall'inizio alla fine, viene chiamata la funzione lambda onToggleDone, passando l'attuale todoItem. Questo corrisponde all'aggiornamento dell'elemento della lista di cose da fare.
    • Se l'elemento viene strisciato dall'inizio alla fine, viene chiamata la funzione lambda onRemove, passando l'attuale todoItem. Ciò corrisponde all'eliminazione dell'elemento della lista di cose da fare.
    • it != StartToEnd: questa riga restituisce true se la direzione dello scorrimento non è StartToEnd e false in caso contrario. Il ritorno a false impedisce che SwipeToDismissBox scompaia immediatamente dopo uno scorrimento "Attiva/Disattiva completato", consentendo una conferma visiva o un'animazione.
  • SwipeToDismissBox consente le interazioni con 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 backgroundContent ricevono i vincoli completi del contenitore principale per il rendering. Il content viene disegnato sopra il backgroundContent. In questo caso:
    • backgroundContent è implementato come Icon con un colore di sfondo basato su SwipeToDismissBoxValue:
    • Blue quando scorri StartToEnd: attiva/disattiva un elemento della lista di cose da fare.
    • Red quando scorri EndToStart: eliminazione di un elemento della lista di cose da fare.
    • Non viene visualizzato nulla in background per Settled: quando l'elemento non viene scorretto, non viene visualizzato nulla in background.
    • Allo stesso modo, il Icon visualizzato si adatta alla direzione dello scorrimento:
    • StartToEnd mostra un'icona CheckBox quando l'elemento della lista di cose da fare è stato completato e un'icona CheckBoxOutlineBlank quando non è stato completato.
    • EndToStart viene visualizzata 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 del codice

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

Risultato

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

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

Per il codice di esempio completo, consulta il file di origine di GitHub.

Esempio avanzato: animare il colore di sfondo con lo scorrimento

Gli snippet riportati di seguito mostrano come incorporare una soglia di posizione per animare il colore di sfondo di un elemento quando viene scorretto.

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 del codice

  • drawBehind viene disegnato direttamente nella tela dietro i contenuti del composable Icon.
    • drawRect() disegna un rettangolo sul canvas e riempie l'intero ambito del disegno con il colore Color specificato.
  • Quando scorri, il colore di sfondo dell'elemento passa gradualmente da un colore all'altro utilizzandolerp.
    • Se scorri da StartToEnd, il colore di sfondo cambia gradualmente da grigio chiaro a blu.
    • Se scorri da EndToStart, il colore di sfondo cambia gradualmente da grigio chiaro a rosso.
    • L'entità della transizione da un colore all'altro è determinata da swipeToDismissBoxState.progress.
  • OutlinedCard aggiunge una sottile separazione visiva tra le voci 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 del codice

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

Risultato

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

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

Per il codice di esempio completo, consulta il file di origine di GitHub.

Risorse aggiuntive