Desliza el dedo para descartar o actualizar.

El componente SwipeToDismissBox permite que un usuario descarte o actualice un elemento deslizándolo hacia la izquierda o la derecha.

Plataforma de la API

Usa el elemento componible SwipeToDismissBox para implementar acciones que se activan con gestos de deslizamiento. Entre los parámetros clave, se incluyen los siguientes:

  • state: Es el estado SwipeToDismissBoxState creado para almacenar el valor que producen los cálculos en el elemento de deslizamiento, que activa eventos cuando se produce.
  • backgroundContent: Es un elemento componible personalizable que se muestra detrás del contenido del elemento y que se revela cuando se desliza el contenido.

Ejemplo básico: Actualiza o descarta con un deslizamiento

Los fragmentos de este ejemplo muestran una implementación de deslizamiento que actualiza el elemento cuando se desliza de un extremo al otro o lo descarta cuando se desliza de un extremo al otro.

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

Puntos clave sobre el código

  • swipeToDismissBoxState administra el estado del componente. Activa la devolución de llamada confirmValueChange una vez que se completa la interacción con el elemento. El cuerpo de la devolución de llamada controla las diferentes acciones posibles. La devolución de llamada muestra un valor booleano que le indica al componente si debe mostrar una animación de descarte. En este caso:
    • Si se desliza el elemento de un extremo al otro, llama a la lambda onToggleDone y pasa el todoItem actual. Esto corresponde a la actualización del elemento de tareas pendientes.
    • Si se desliza el elemento de un extremo al otro, llama a la lambda onRemove y pasa el todoItem actual. Esto corresponde a borrar el elemento de tareas pendientes.
    • it != StartToEnd: Esta línea muestra true si la dirección del deslizamiento no es StartToEnd y false de lo contrario. Devolver false evita que SwipeToDismissBox desaparezca de inmediato después de un deslizamiento de "activar/desactivar", lo que permite una confirmación o animación visual.
  • SwipeToDismissBox habilita interacciones de deslizamiento horizontal en cada elemento. En reposo, muestra el contenido interno del componente, pero cuando un usuario comienza a deslizar el dedo, el contenido se aleja y aparece el backgroundContent. Tanto el contenido normal como el backgroundContent obtienen las restricciones completas del contenedor superior para renderizarse. El content se dibuja sobre el backgroundContent. En este caso:
    • backgroundContent se implementa como un Icon con un color de fondo basado en SwipeToDismissBoxValue:
    • Blue cuando se desliza StartToEnd: Activa o desactiva una tarea pendiente.
    • Red cuando se desliza EndToStart: Borra una tarea pendiente.
    • No se muestra nada en segundo plano para Settled; cuando no se desliza el elemento, no se muestra nada en segundo plano.
    • Del mismo modo, el Icon que se muestra se adapta a la dirección del deslizamiento:
    • StartToEnd muestra un ícono de CheckBox cuando se completa la tarea y un ícono de CheckBoxOutlineBlank cuando no se completa.
    • EndToStart muestra un ícono 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()
            )
        }
    }
}

Puntos clave sobre el código

  • mutableStateListOf(...) crea una lista observable que puede contener objetos TodoItem. Cuando se agrega o quita un elemento de esta lista, Compose recompone las partes de la IU que dependen de él.
    • Dentro de mutableStateListOf(), se inicializan cuatro objetos TodoItem con sus respectivas descripciones: “Pagar facturas”, “Comprar comestibles”, “Ir al gimnasio” y “Comer”.
  • LazyColumn muestra una lista de todoItems con desplazamiento vertical.
  • onToggleDone = { todoItem -> ... } es una función de devolución de llamada que se invoca desde TodoListItem cuando el usuario marca un objeto como terminado. Actualiza la propiedad isItemDone de todoItem. Como todoItems es un mutableStateListOf, este cambio activa una recomposición y actualiza la IU.
  • onRemove = { todoItem -> ... } es una función de devolución de llamada que se activa cuando el usuario quita el elemento. Quita el todoItem específico de la lista todoItems. Esto también causa una recomposición, y el elemento se quitará de la lista que se muestra.
  • Se aplica un modificador animateItem a cada TodoListItem para que se llame a placementSpec del modificador cuando se descarte el elemento. Esto anima la eliminación del elemento, así como la reorganización de otros elementos de la lista.

Resultado

En el siguiente video, se muestra la funcionalidad básica de deslizar para descartar de los fragmentos anteriores:

Figura 1. Una implementación básica del gesto de deslizar para descartar que puede marcar un elemento como completado y mostrar una animación de descarte para un elemento en una lista.

Consulta el archivo fuente de GitHub para ver el código de muestra completo.

Ejemplo avanzado: Anima el color de fondo al deslizar el dedo

En los siguientes fragmentos, se muestra cómo incorporar un umbral de posición para animar el color de fondo de un elemento cuando se desliza el dedo.

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

Puntos clave sobre el código

  • drawBehind dibuja directamente en el lienzo detrás del contenido del elemento componible Icon.
    • drawRect() dibuja un rectángulo en el lienzo y completa todos los límites del alcance de dibujo con el Color especificado.
  • Cuando se desliza el dedo, el color de fondo del elemento cambia de forma fluida con lerp.
    • Para un deslizamiento desde StartToEnd, el color de fondo cambia gradualmente de gris claro a azul.
    • Para un deslizamiento desde EndToStart, el color de fondo cambia gradualmente de gris claro a rojo.
    • La cantidad de transición de un color al siguiente se determina mediante swipeToDismissBoxState.progress.
  • OutlinedCard agrega una separación visual sutil entre los elementos de la 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()
            )
        }
    }
}

Puntos clave sobre el código

  • Para obtener información sobre los puntos clave de este código, consulta Puntos clave en una sección anterior, que describe un fragmento de código idéntico.

Resultado

En el siguiente video, se muestra la funcionalidad avanzada con color de fondo animado:

Figura 2. Implementación de deslizamiento para revelar o borrar, con colores de fondo animados y un umbral más largo antes de que se registre la acción.

Consulta el archivo fuente de GitHub para ver el código de muestra completo.

Recursos adicionales