Deslize para dispensar ou atualizar

O componente SwipeToDismissBox permite que o usuário dispense ou atualize um item deslizando-o para a esquerda ou direita.

Superfície da API

Use o elemento combinável SwipeToDismissBox para implementar ações acionadas por gestos de deslizar. Os principais parâmetros incluem:

  • state: o estado SwipeToDismissBoxState criado para armazenar o valor produzido por cálculos no item de deslizar, que aciona eventos quando produzido.
  • backgroundContent: um elemento combinável personalizável exibido atrás do conteúdo do item, que é revelado quando o conteúdo é deslizado.

Exemplo básico: atualizar ou dispensar ao deslizar

Os snippets neste exemplo mostram uma implementação de deslize que atualiza o item quando deslizado do início para o fim ou dispensa o item quando deslizado do fim para o início.

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

Pontos principais sobre o código

  • swipeToDismissBoxState gerencia o estado do componente. Ele aciona o callback confirmValueChange quando a interação com o item é concluída. O corpo do callback processa as diferentes ações possíveis. O callback retorna um booleano que informa ao componente se ele precisa mostrar uma animação de encerramento. Nesse caso:
    • Se o item for deslizado do início ao fim, ele vai chamar o lambda onToggleDone, transmitindo o todoItem atual. Isso corresponde à atualização do item de tarefas.
    • Se o item for deslizado do fim para o início, ele vai chamar o lambda onRemove, transmitindo o todoItem atual. Isso corresponde à exclusão do item de tarefas.
    • it != StartToEnd: essa linha retorna true se a direção do deslizar não for StartToEnd e false caso contrário. O retorno de false impede que o SwipeToDismissBox desapareça imediatamente após um deslize de "alternar concluído", permitindo uma confirmação visual ou animação.
  • SwipeToDismissBox ativa interações de deslizar horizontal em cada item. Em repouso, ele mostra o conteúdo interno do componente, mas quando um usuário começa a deslizar, o conteúdo é removido e o backgroundContent aparece. Tanto o conteúdo normal quanto o backgroundContent recebem as restrições completas do contêiner pai para renderização. O content é desenhado sobre o backgroundContent. Nesse caso:
    • backgroundContent é implementado como uma Icon com uma cor de plano de fundo baseada em SwipeToDismissBoxValue:
    • Blue ao deslizar StartToEnd: alternar um item de tarefas.
    • Red ao deslizar EndToStart: excluir uma tarefa.
    • Nada é exibido no segundo plano para Settled. Quando o item não está sendo deslizado, nada é mostrado no segundo plano.
    • Da mesma forma, o Icon exibido se adapta à direção do movimento:
    • StartToEnd mostra um ícone CheckBox quando a tarefa é concluída e um ícone CheckBoxOutlineBlank quando ela não é concluída.
    • EndToStart exibe um ícone 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()
            )
        }
    }
}

Pontos principais sobre o código

  • mutableStateListOf(...) cria uma lista observável que pode conter objetos TodoItem. Quando um item é adicionado ou removido dessa lista, o Compose recompone as partes da IU que dependem dele.
    • Dentro de mutableStateListOf(), quatro objetos TodoItem são inicializados com as respectivas descrições: "Pay bills", "Buy groceries", "Go to gym" e "Get dinner".
  • LazyColumn mostra uma lista de todoItems com rolagem vertical.
  • onToggleDone = { todoItem -> ... } é uma função de callback invocada em TodoListItem quando o usuário marca um objeto como concluído. Ele atualiza a propriedade isItemDone do todoItem. Como todoItems é um mutableStateListOf, essa mudança aciona uma recomposição, atualizando a interface.
  • onRemove = { todoItem -> ... } é uma função de callback acionada quando o usuário remove o item. Ele remove o todoItem específico da lista todoItems. Isso também causa uma recomposição, e o item será removido da lista exibida.
  • Um modificador animateItem é aplicado a cada TodoListItem para que o placementSpec do modificador seja chamado quando o item for dispensado. Isso anima a remoção do item, bem como a reordenação de outros itens na lista.

Resultado

O vídeo a seguir demonstra a funcionalidade básica de deslizar para descartar dos snipets anteriores:

Figura 1. Uma implementação básica de deslizar para descartar que pode marcar um item como concluído e mostrar uma animação de descarte para um item em uma lista.

Consulte o arquivo de origem do GitHub para conferir o código de exemplo completo.

Exemplo avançado: animar a cor do plano de fundo ao deslizar

Os snippets a seguir mostram como incorporar um limite posicional para animar a cor de fundo de um item ao deslizar.

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

Pontos principais sobre o código

  • O drawBehind é exibido diretamente na tela por trás do conteúdo do elemento combinável Icon.
    • drawRect() desenha um retângulo na tela e preenche todos os limites do escopo de exibição com o Color especificado.
  • Ao deslizar, a cor de plano de fundo do item muda suavemente usando lerp.
    • Em um deslize de StartToEnd, a cor de fundo muda gradualmente de cinza claro para azul.
    • Para um deslize de EndToStart, a cor de fundo muda gradualmente de cinza claro para vermelho.
    • A quantidade de transição de uma cor para a próxima é determinada por swipeToDismissBoxState.progress.
  • O OutlinedCard adiciona uma separação visual sutil entre os itens da lista.

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

Pontos principais sobre o código

  • Para conferir os principais pontos sobre esse código, consulte Principais pontos em uma seção anterior, que descreve um snippet de código idêntico.

Resultado

O vídeo a seguir mostra a funcionalidade avançada com a cor de plano de fundo animada:

Figura 2. Uma implementação de deslizar para revelar ou excluir, com cores de plano de fundo animadas e um limite mais longo antes que a ação seja registrada.

Consulte o arquivo de origem do GitHub para conferir o código de exemplo completo.

Outros recursos