Hay varios términos y conceptos que es importante comprender cuando se trabaja en el control gestual en una aplicación. En esta página, se explican los términos punteros, eventos de puntero y gestos, y se presentan los diferentes niveles de abstracción para los gestos. También profundiza en el consumo y la propagación de eventos.
Definiciones
Para comprender los diversos conceptos que se mencionan en esta página, debes comprender parte de la terminología que se usa:
- Puntero: Es un objeto físico que puedes usar para interactuar con tu aplicación.
En los dispositivos móviles, el puntero más común es el dedo que interactúa con la pantalla táctil. También puedes usar una pluma stylus para reemplazar el dedo.
En el caso de pantallas grandes, puedes usar un mouse o un panel táctil para interactuar con la pantalla de forma indirecta. Un dispositivo de entrada debe poder "apuntar" a una coordenada para que se considere un puntero, por lo que un teclado, por ejemplo, no puede considerarse un puntero. En Compose, el tipo de puntero se incluye en los cambios de punteros con
PointerType
. - Evento de puntero: Describe una interacción de bajo nivel de uno o más punteros con la aplicación en un momento determinado. Cualquier interacción con el puntero, como colocar un dedo en la pantalla o arrastrar un mouse, activará un evento. En Compose, toda la información relevante para ese evento se encuentra en la clase
PointerEvent
. - Gesto: Una secuencia de eventos de puntero que se pueden interpretar como una sola acción. Por ejemplo, un gesto de toque puede considerarse una secuencia de un evento de presión seguido de un evento de presión. Hay gestos comunes que usan muchas apps, como presionar, arrastrar o transformar, pero también puedes crear tu propio gesto personalizado cuando lo necesites.
Diferentes niveles de abstracción
Jetpack Compose proporciona diferentes niveles de abstracción para controlar los gestos.
En el nivel superior, se encuentra la compatibilidad con componentes. Los elementos componibles como Button
incluyen automáticamente compatibilidad con gestos. Para agregar compatibilidad con gestos a componentes personalizados, puedes agregar modificadores de gestos, como clickable
, a elementos arbitrarios que admiten composición. Por último, si necesitas un gesto personalizado, puedes usar el modificador pointerInput
.
Como regla, compila en el nivel más alto de abstracción que ofrezca la funcionalidad que necesitas. De esta manera, te beneficias de las prácticas recomendadas incluidas en la capa. Por ejemplo, Button
contiene más información semántica, que se usa para la accesibilidad, que clickable
, que contiene más información que una implementación de pointerInput
sin procesar.
Compatibilidad con componentes
Muchos componentes listos para usar en Compose incluyen algún tipo de control interno de gestos. Por ejemplo, un objeto LazyColumn
responde a los gestos de arrastre desplazándose por su contenido, un objeto Button
muestra una ondulación cuando presionas hacia abajo y el componente SwipeToDismiss
incluye la lógica de deslizamiento para descartar un elemento. Este tipo de control gestual funciona automáticamente.
Además del control gestual interno, muchos componentes también requieren que el llamador controle el gesto. Por ejemplo, un objeto Button
detecta automáticamente toques y activa un evento de clic. Pasa una lambda onClick
a Button
para reaccionar al gesto. De manera similar, agregas una lambda onValueChange
a una Slider
para reaccionar cuando el usuario arrastre el controlador del control deslizante.
Cuando se adapte a tu caso de uso, prioriza los gestos incluidos en los componentes, ya que incluyen compatibilidad inmediata para el enfoque y la accesibilidad, y están bien probados. Por ejemplo, un Button
se marca de una manera especial para que los servicios de accesibilidad lo describan correctamente como un botón, en lugar de cualquier elemento en el que se pueda hacer clic:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
Para obtener más información sobre la accesibilidad en Compose, consulta Accesibilidad en Compose.
Cómo agregar gestos específicos a elementos componibles arbitrarios con modificadores
Puedes aplicar modificadores de gestos a cualquier elemento componible arbitrario para que este escuche los gestos. Por ejemplo, puedes permitir que una Box
genérica controle los gestos de presión si la estableces como clickable
, o bien puedes permitir que una Column
controle el desplazamiento vertical aplicando verticalScroll
.
Hay muchos modificadores para controlar diferentes tipos de gestos:
- Controla las presiones y presiones con los modificadores
clickable
,combinedClickable
,selectable
,toggleable
ytriStateToggleable
. - Controla el desplazamiento con los modificadores
horizontalScroll
,verticalScroll
yscrollable
más genéricos. - Controla el arrastre con los modificadores
draggable
yswipeable
. - Controla los gestos de varios toques, como el desplazamiento lateral, la rotación y el zoom, con el modificador
transformable
.
Como regla, es preferible usar modificadores de gestos listos para usar en lugar del control gestual personalizado.
Los modificadores agregan más funcionalidad además del control puro de eventos de puntero.
Por ejemplo, el modificador clickable
no solo agrega detección de pulsaciones y presiones, sino que también agrega información semántica, indicaciones visuales sobre interacciones, desplazamiento, enfoque y compatibilidad con el teclado. Puedes consultar el código fuente de clickable
para ver cómo se agrega la funcionalidad.
Agrega un gesto personalizado a elementos componibles arbitrarios con el modificador pointerInput
No todos los gestos se implementan con un modificador de gestos listo para usar. Por ejemplo, no puedes usar un modificador para reaccionar a un arrastre después de mantener presionado, hacer clic con el control o presionar con tres dedos. En su lugar, puedes escribir tu propio controlador de gestos para identificar estos gestos personalizados. Puedes crear un controlador de gestos con el modificador pointerInput
, que te brinda acceso a los eventos del puntero sin procesar.
El siguiente código escucha los eventos de puntero sin procesar:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
Si divides este fragmento, los componentes principales son los siguientes:
- El modificador
pointerInput
Le pasas una o más claves. Cuando cambia el valor de una de esas teclas, se vuelve a ejecutar la expresión lambda del contenido del modificador. En la muestra, se pasa un filtro opcional al elemento componible. Si el valor de ese filtro cambia, el controlador de eventos del puntero debe volver a ejecutarse para asegurarte de que se registren los eventos correctos. awaitPointerEventScope
crea un alcance de corrutinas que se puede usar para esperar eventos del puntero.awaitPointerEvent
suspende la corrutina hasta que se produce un próximo evento de puntero.
Aunque escuchar eventos de entrada sin procesar es potente, también es complejo escribir un gesto personalizado basado en estos datos sin procesar. Para simplificar la creación de gestos personalizados, hay muchos métodos de utilidad disponibles.
Cómo detectar gestos completos
En lugar de controlar los eventos del puntero sin procesar, puedes escuchar para que ocurran gestos específicos y responder de manera apropiada. AwaitPointerEventScope
proporciona métodos para escuchar lo siguiente:
- Presiona, presiona dos veces y mantén presionado:
detectTapGestures
- Arrastrar:
detectHorizontalDragGestures
,detectVerticalDragGestures
,detectDragGestures
ydetectDragGesturesAfterLongPress
- Transformaciones:
detectTransformGestures
Estos son detectores de nivel superior, por lo que no puedes agregar varios detectores dentro de un modificador pointerInput
. En el siguiente fragmento, solo se detectan los toques, no los arrastres:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
De forma interna, el método detectTapGestures
bloquea la corrutina, y nunca se alcanza el segundo detector. Si necesitas agregar más de un objeto de escucha de gestos a un elemento componible, usa instancias independientes del modificador pointerInput
:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
Cómo controlar eventos por gesto
Por definición, los gestos comienzan con un evento de puntero hacia abajo. Puedes usar el método auxiliar awaitEachGesture
en lugar del bucle while(true)
que pasa por cada evento sin procesar. El método awaitEachGesture
reinicia el bloque que lo contiene cuando se levantan todos los punteros, lo que indica que se completó el gesto:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
En la práctica, casi siempre querrás usar awaitEachGesture
, a menos que respondas a los eventos del puntero sin identificar los gestos. Un ejemplo de esto es hoverable
, que no responde a los eventos del puntero hacia abajo o hacia arriba; solo necesita saber cuándo un puntero entra o sale de sus límites.
Esperar un evento o un gesto secundario específico
Hay un conjunto de métodos que ayudan a identificar las partes comunes de los gestos:
- Suspende hasta que un puntero caiga con
awaitFirstDown
o espera a que todos los punteros suban conwaitForUpOrCancellation
. - Crea un objeto de escucha de arrastre de bajo nivel con
awaitTouchSlopOrCancellation
yawaitDragOrCancellation
. El controlador de gestos primero se suspende hasta que el puntero alcanza la inclinación táctil y, luego, se suspende hasta que aparece un primer evento de arrastre. Si solo te interesan los arrastres a lo largo de un solo eje, usaawaitHorizontalTouchSlopOrCancellation
másawaitHorizontalDragOrCancellation
, o bienawaitVerticalTouchSlopOrCancellation
másawaitVerticalDragOrCancellation
. - Suspende hasta que se mantenga presionado con
awaitLongPressOrCancellation
. - Usa el método
drag
para escuchar de forma continua los eventos de arrastre, o bienhorizontalDrag
overticalDrag
para escuchar los eventos de arrastre en un eje.
Aplicar cálculos para eventos de varios puntos de contacto
Cuando un usuario realiza un gesto de varios toques con más de un puntero, es complejo comprender la transformación requerida en función de los valores sin procesar.
Si el modificador transformable
o los métodos detectTransformGestures
no proporcionan un control lo suficientemente detallado para tu caso de uso, puedes escuchar los eventos sin procesar y aplicar cálculos sobre ellos. Estos métodos auxiliares son calculateCentroid
, calculateCentroidSize
, calculatePan
, calculateRotation
y calculateZoom
.
Envío de eventos y prueba de posicionamiento
No todos los eventos de puntero se envían a cada modificador pointerInput
. El envío de eventos funciona de la siguiente manera:
- Los eventos del puntero se envían a una jerarquía componible. En el momento en que un puntero nuevo activa su primer evento de puntero, el sistema comienza a probar los elementos que admiten composición "elegibles". Un elemento componible se considera apto cuando tiene capacidades de control de entrada de puntero. Las pruebas de posicionamiento fluyen desde la parte superior del árbol de IU hasta la parte inferior. Un elemento componible es "acierto" cuando el evento del puntero ocurre dentro de los límites de ese elemento. Este proceso da como resultado una cadena de elementos componibles que hacen una prueba de posicionamiento positiva.
- De forma predeterminada, cuando hay varios elementos componibles aptos en el mismo nivel del árbol, solo el elemento con el índice z más alto tendrá el valor "hit". Por ejemplo, cuando agregas dos elementos componibles
Button
superpuestos a unBox
, solo el dibujado en la parte superior recibe eventos de puntero. En teoría, puedes anular este comportamiento si creas tu propia implementación dePointerInputModifierNode
y configurassharePointerInputWithSiblings
como verdadero. - Los eventos adicionales del mismo puntero se despachan a esa misma cadena de elementos componibles y fluyen según la lógica de propagación de eventos. El sistema no realizará más pruebas de posicionamiento para este puntero. Eso significa que cada elemento componible de la cadena recibe todos los eventos de ese puntero, incluso cuando aquellos ocurren fuera de los límites de ese elemento. Los elementos componibles que no están en la cadena nunca reciben eventos de puntero, incluso cuando el puntero está dentro de sus límites.
Los eventos de colocar el cursor sobre un elemento, que se activan con un mouse o una pluma stylus, son una excepción a las reglas que se definen aquí. Los eventos de colocar el cursor sobre un elemento se envían a cualquier elemento de componibilidad que alcance. Por lo tanto, cuando un usuario coloca el cursor sobre los límites de un elemento componible al siguiente, en lugar de enviar los eventos a ese primer elemento componible, los eventos se envían al nuevo elemento componible.
Consumo de eventos
Cuando más de un elemento componible tiene un controlador de gestos asignado, esos controladores no deben entrar en conflicto. Por ejemplo, observa esta IU:
Cuando un usuario presiona el botón de favoritos, la lambda onClick
del botón controla ese gesto. Cuando un usuario presiona cualquier otra parte del elemento de la lista, ListItem
controla ese gesto y navega al artículo. En términos de entrada del puntero, el botón debe consumir este evento para que su elemento superior sepa no volver a reaccionar a él. Los gestos incluidos en los componentes listos para usar y los modificadores de gestos comunes incluyen este comportamiento de consumo. Sin embargo, si escribes tu propio gesto personalizado, debes consumir eventos de forma manual. Para ello, usa el método PointerInputChange.consume
:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
El consumo de un evento no detiene su propagación a otros elementos componibles. Un elemento que admite composición debe ignorar de forma explícita los eventos consumidos. Cuando escribes gestos personalizados, debes verificar si otro elemento ya consumió un evento:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
Propagación de eventos
Como se mencionó antes, los cambios de puntero se pasan a cada elemento componible que recibe.
Sin embargo, si existe más de un elemento componible, ¿en qué orden se propagan los eventos? Si tomas el ejemplo de la última sección, esta IU se traduce al siguiente árbol de IU, en el que solo ListItem
y Button
responden a los eventos de puntero:
Los eventos del puntero fluyen a través de cada uno de estos elementos componibles tres veces, durante tres "pases":
- En el Pase inicial, el evento fluye desde la parte superior del árbol de IU hasta la parte inferior. Este flujo permite que un elemento superior intercepte un evento antes de que el secundario pueda consumirlo. Por ejemplo, la información sobre la herramienta debe interceptar una presión prolongada en lugar de pasarla a sus elementos secundarios. En nuestro
ejemplo,
ListItem
recibe el evento antes deButton
. - En el Pase principal, el evento fluye desde los nodos hoja del árbol de IU hasta la raíz del árbol de IU. En esta fase, sueles consumir gestos y es el pase predeterminado cuando escuchas eventos. El control de los gestos en este pase implica que los nodos de hoja tienen prioridad sobre los superiores, que es el comportamiento más lógico de la mayoría de los gestos. En nuestro ejemplo,
Button
recibe el evento antes deListItem
. - En el Pase final, el evento fluye una vez más desde la parte superior del árbol de IU hasta los nodos de hoja. Este flujo permite que los elementos superiores en la pila respondan al consumo de eventos por parte de su elemento superior. Por ejemplo, un botón quita su indicación de ondas cuando una pulsación se convierte en un arrastre de su elemento superior desplazable.
Visualmente, el flujo de eventos se puede representar de la siguiente manera:
Una vez que se consume un cambio de entrada, la información se pasa desde ese punto en el flujo en adelante:
En el código, puedes especificar el pase que te interesa:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
En este fragmento de código, cada una de estas llamadas de método en espera muestra el mismo evento idéntico, aunque podrían haber cambiado los datos sobre el consumo.
Cómo probar gestos
En tus métodos de prueba, puedes enviar eventos de puntero de forma manual con el método performTouchInput
. De esta manera, puedes realizar gestos completos de nivel superior (como pellizcar o clic largo) o gestos de bajo nivel (como mover el cursor una determinada cantidad de píxeles):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Consulta la documentación de performTouchInput
para ver más ejemplos.
Más información
Puedes obtener más información sobre los gestos en Jetpack Compose en los siguientes recursos:
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Accesibilidad en Compose
- Desplazamiento
- Presionar y presionar