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.

Superficie de la API

Usa el elemento de componibilidad 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 producido por los cálculos en el elemento deslizable, que activa eventos cuando se produce.
  • backgroundContent: Es un elemento componible personalizable que se muestra detrás del contenido del elemento 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 principio a fin o lo descarta cuando se desliza de fin a principio.

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(
                        painter = rememberVectorPainter(if (todoItem.isItemDone) AppIcons.CheckBox else AppIcons.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(
                        painter = rememberVectorPainter(AppIcons.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, ocurre lo siguiente:
    • Si se desliza el elemento de principio a fin, se llama a la lambda onToggleDone y se pasa el todoItem actual. Esto corresponde a la actualización del elemento de tareas pendientes.
    • Si el elemento se desliza 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 devuelve true si la dirección de deslizamiento no es StartToEnd, y false en caso contrario. Si se devuelve false, se evita que el SwipeToDismissBox desaparezca de inmediato después de un deslizamiento para "marcar como completado", lo que permite una confirmación visual o una animación.
  • SwipeToDismissBox habilita las 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 quita 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, ocurre lo siguiente:
    • backgroundContent se implementa como un Icon con un color de fondo basado en SwipeToDismissBoxValue:
    • Blue cuando se desliza el dedo hacia StartToEnd: se activa o desactiva un elemento de tareas pendientes.
    • Red cuando se desliza EndToStart: se borra un elemento de la lista de tareas pendientes.
    • 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 el elemento de tareas pendientes y un ícono de CheckBoxOutlineBlank cuando no se completa.
    • EndToStart muestra un ícono de Delete.

@Composable
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 ella.
    • Dentro de mutableStateListOf(), se inicializan cuatro objetos TodoItem con sus respectivas descripciones: "Pay bills", "Buy groceries", "Go to gym" y "Get dinner".
  • LazyColumn muestra una lista de desplazamiento vertical de todoItems.
  • onToggleDone = { todoItem -> ... } es una función de devolución de llamada que se invoca desde TodoListItem cuando el usuario marca un objeto como completado. Actualiza la propiedad isItemDone de todoItem. Como todoItems es un mutableStateListOf, este cambio activa una recomposición, lo que 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 provoca 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 al placementSpec del modificador cuando se descarte el elemento. Esto anima la eliminación del elemento, así como el reordenamiento 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 de deslizar para descartar que puede marcar un elemento como completado y mostrar una animación de descarte para un elemento de una lista

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

Ejemplo avanzado: Anima el color de fondo cuando se desliza el dedo

En los siguientes fragmentos, se muestra cómo incorporar un umbral posicional 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(
                        painter = rememberVectorPainter(if (todoItem.isItemDone) AppIcons.CheckBox else AppIcons.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(
                        painter = rememberVectorPainter(AppIcons.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 del 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 con 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 conocer los puntos clave sobre este código, consulta Puntos clave de una sección anterior, en la que se describe un fragmento de código idéntico.

Resultado

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

Figura 2. Es una 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 de origen de GitHub para ver el código de muestra completo.

Recursos adicionales