Cómo controlar las interacciones del usuario

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

Los componentes de la interfaz de usuario le proporcionan comentarios al usuario del dispositivo en función de cómo responde a las interacciones. Cada componente tiene su propia forma de responder a las interacciones, lo que ayuda al usuario a saber qué hacen sus interacciones. Por ejemplo, si un usuario toca un botón en la pantalla táctil de un dispositivo, es probable que este cambie de alguna manera, quizás agregando un color de resaltado. Este cambio le informa al usuario que tocó el botón. Si el usuario no quiso hacerlo, sabrá que debe arrastrar el dedo fuera del botón antes de soltarlo. De lo contrario, este se activará.

En la documentación de Gestos de Compose, se explica cómo los componentes de Compose controlan un evento de puntero de bajo nivel, como los movimientos y los clics del puntero. Desde el primer momento, Compose abstrae esos eventos de bajo nivel en interacciones de nivel superior; por ejemplo, una serie de eventos de puntero podría sumar una acción de presionar y soltar. Entender esas abstracciones de nivel superior puede ayudarte a personalizar la manera en que responde tu IU al usuario. Por ejemplo, es posible que quieras personalizar cómo cambia la apariencia de un componente cuando el usuario interactúa con él o quizás solo quieres mantener un registro de esas acciones del usuario. Este documento te brinda la información que necesitas para modificar los elementos estándar de la IU o diseñar los propios.

Interacciones

En muchos casos, no necesitas saber la forma exacta en la que el componente de Compose interpreta las interacciones del usuario. Por ejemplo, Button se basa en Modifier.clickable para determinar si el usuario hizo clic en el botón. Si agregas un botón típico a tu app, puedes definir el código onClick de ese botón, y Modifier.clickable ejecutará ese código cuando corresponda. Eso significa que no necesitas saber si el usuario presionó la pantalla o seleccionó el botón con un teclado. Modifier.clickable determina que el usuario hizo un clic y responde ejecutando el código onClick.

Sin embargo, si deseas personalizar la respuesta de tu componente de IU al comportamiento del usuario, es posible que necesites saber más acerca de lo que ocurre en niveles más profundos. En esta sección, encontrarás parte de esa información.

Cuando un usuario interactúa con un componente de la IU, el sistema representa su comportamiento mediante la generación de varios eventos Interaction. Por ejemplo, si un usuario toca un botón, este genera PressInteraction.Press. Si el usuario levanta el dedo dentro del botón, se genera un PressInteraction.Release que informa que terminó el clic. Por otro lado, si el usuario arrastra el dedo fuera del botón y luego lo levanta, el botón genera PressInteraction.Cancel para indicar que no se completó la acción de presionar el botón.

Estas interacciones no tienen tendencias. Es decir, estos eventos de interacción de bajo nivel no pretenden interpretar el significado de las acciones del usuario ni su secuencia. Tampoco interpretan qué acciones del usuario pueden tener prioridad sobre otras.

Estas interacciones suelen venir en pares, con un inicio y un fin. La segunda interacción contiene una referencia a la primera. Por ejemplo, si un usuario toca un botón y luego levanta el dedo, el toque genera una interacción PressInteraction.Press y levantarlo genera una PressInteraction.Release; la Release tiene una propiedad press que identifica la PressInteraction.Press inicial.

Puedes ver las interacciones de un componente en particular si observas su InteractionSource. InteractionSource se basa en flujos de Kotlin, por lo que puedes recopilar las interacciones a partir de allí de la misma manera en que trabajarías con cualquier otro flujo.

Estado de las interacciones

Se recomienda que realices un seguimiento de las interacciones tú mismo para extender la funcionalidad integrada de tus componentes. Por ejemplo, tal vez quieras que un botón cambie de color cuando se presiona. La forma más sencilla de hacer un seguimiento de las interacciones es observar el estado de interacción apropiado. InteractionSource ofrece una serie de métodos que revelan varios estados de interacción como estados. Por ejemplo, si quieres ver si se presiona un botón en particular, puedes llamar a su método InteractionSource.collectIsPressedAsState():

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(
    onClick = { /* do something */ },
    interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Además de collectIsPressedAsState(), Compose también proporciona collectIsFocusedAsState(), collectIsDraggedAsState() y collectIsHoveredAsState(). En realidad, estos son métodos prácticos compilados sobre las APIs de InteractionSource de nivel inferior. En algunos casos, es recomendable usar esas funciones de nivel inferior directamente.

Por ejemplo, supón que necesitas saber si se presiona un botón y también si se lo está arrastrando. Si usas collectIsPressedAsState() y collectIsDraggedAsState(), Compose realiza gran parte del trabajo por duplicado y no hay garantía de que obtengas todas las interacciones en el orden correcto. Para este tipo de situaciones, es recomendable trabajar directamente con InteractionSource. En la siguiente sección, se describe cómo hacer un seguimiento de las interacciones por tu cuenta y obtener solo la información que necesitas.

Cómo trabajar con InteractionSource

Si necesitas información detallada sobre las interacciones con un componente, puedes usar las APIs de flujo estándar para el InteractionSource de ese componente. Por ejemplo, supongamos que quieres mantener una lista de las interacciones de presionar y arrastrar para un InteractionSource. Este código realiza la mitad del trabajo y agrega las presiones nuevas a la lista a medida que se reciben:

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
        }
    }
}

Sin embargo, además de agregar las interacciones nuevas, también debes quitarlas cuando terminan (por ejemplo, cuando el usuario levanta el dedo del componente). Es fácil de hacer, ya que las interacciones de fin siempre llevan una referencia a la interacción de inicio asociada. En el siguiente código, se muestra cómo quitar las interacciones que finalizaron:

val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is PressInteraction.Release -> {
                interactions.remove(interaction.press)
            }
            is PressInteraction.Cancel -> {
                interactions.remove(interaction.press)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
            is DragInteraction.Stop -> {
                interactions.remove(interaction.start)
            }
            is DragInteraction.Cancel -> {
                interactions.remove(interaction.start)
            }
        }
    }
}

Ahora, si quieres saber si el componente se está presionando o arrastrando, lo único que debes hacer es comprobar si interactions está vacío:

val isPressedOrDragged = interactions.isNotEmpty()

Si quieres saber cuál fue la interacción más reciente, consulta el último elemento de la lista. Por ejemplo, así es como la implementación de ondas de Compose determina la superposición de estados adecuada para usar en la interacción más reciente:

val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}

Trabajo con un ejemplo

Para ver cómo compilar componentes con una respuesta personalizada a la entrada, a continuación se muestra un ejemplo de un botón modificado. En este caso, supongamos que quieres un botón que responda a las pulsaciones cambiando la apariencia:

Animación de un botón que agrega dinámicamente un ícono cuando se hace clic en él

Para ello, compila un elemento que admite composición personalizado, basado en Button y haz que tome un parámetro icon adicional a fin de dibujar el ícono (en este caso, un carrito de compras). Llamas a collectIsPressedAsState() para realizar un seguimiento a fin de que el usuario pueda colocar el cursor sobre el botón. Cuando eso sucede, agregas el ícono. A continuación, se muestra el código:

@Composable
fun PressIconButton(
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource =
        remember { MutableInteractionSource() },
) {
    val isPressed by interactionSource.collectIsPressedAsState()
    Button(onClick = onClick, modifier = modifier,
        interactionSource = interactionSource) {
        AnimatedVisibility(visible = isPressed) {
            if (isPressed) {
                Row {
                    icon()
                    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                }
            }
        }
        text()
    }
}

Así se ve cómo se usa ese nuevo elemento que admite composición:

PressIconButton(
    onClick = {},
    icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
    text = { Text("Add to cart") }
)

Como este PressIconButton nuevo se compila sobre el Button de Material existente, reacciona a las interacciones del usuario de todas las maneras habituales. Cuando el usuario presiona el botón, cambia su opacidad ligeramente, como ocurre con un Button común de Material. Además, gracias al código nuevo, HoverIconButton agrega un ícono como respuesta dinámica cuando se coloca el cursor sobre el elemento.