Hay varios términos y conceptos que es importante comprender cuando se trabaja en el manejo gestual en una aplicación. En esta página, se explican los términos punteros, eventos de puntero y gestos, y presenta la diferente abstracción para los gestos. También profundiza en el consumo de eventos de los datos.
Definiciones
Para entender los diversos conceptos de esta página, debes comprender algunos de la terminología utilizada:
- 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 pantallas grandes, puedes usar un mouse o un panel táctil para interactuar de forma indirecta con
la pantalla. Un dispositivo de entrada debe poder “apuntar” en una coordenada para
un puntero, así que un teclado, por ejemplo, no puede ser
puntero. En Compose, el tipo de puntero se incluye en los cambios de puntero 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 dado. Cualquier interacción del puntero, como colocar
un dedo en la pantalla o arrastrar un mouse, activaría un evento. En
Compose, toda la información relevante para un evento de este tipo se encuentra en la
PointerEvent
. - Gesto: Es una secuencia de eventos de puntero que se pueden interpretar como una sola acción. Por ejemplo, un gesto de presión puede considerarse una secuencia de una seguido de un evento ascendente. Hay gestos comunes que usan muchos como tocar, arrastrar o transformar, pero también puedes crear tus propias gesto cuando sea necesario.
Diferentes niveles de abstracción
Jetpack Compose proporciona diferentes niveles de abstracción para controlar gestos.
En el nivel superior, se encuentra la compatibilidad con componentes. Elementos componibles como Button
incluyen automáticamente compatibilidad con gestos. Cómo agregar compatibilidad con gestos al
puedes agregar modificadores de gestos, como clickable
, a valores
elementos componibles. Por último, si necesitas un gesto personalizado, puedes usar
modificador pointerInput
.
Como regla, compila sobre el nivel más alto de abstracción que ofrece la
la funcionalidad que necesitas. Así, podrás aprovechar las prácticas recomendadas
en la capa. Por ejemplo, Button
contiene más información semántica que se usa para
accesibilidad, que clickable
, que contiene más información que una versión sin procesar
Implementación de pointerInput
.
Compatibilidad de los componentes
Muchos componentes listos para usar de Compose incluyen algún tipo de gesto interno
de datos. Por ejemplo, un LazyColumn
responde a los gestos de arrastre de
desplazando su contenido, una Button
muestra un eco cuando lo presionas,
y el componente SwipeToDismiss
contiene la lógica de deslizamiento para descartar un
. Este tipo de control gestual funciona automáticamente.
Además del control gestual interno, muchos componentes también requieren que el llamador
controlar el gesto. Por ejemplo, un Button
detecta automáticamente los toques
y activa un evento de clic. Debes pasar una lambda onClick
al Button
para
reaccionar al gesto. De manera similar, agregas una lambda onValueChange
a una
Slider
para reaccionar cuando el usuario arrastra 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 son
se han probado. Por ejemplo, un Button
se marca de una manera especial para que
los servicios de accesibilidad lo describen correctamente como un botón, en lugar de ser cualquier
elemento en el que se puede 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
Agrega gestos específicos a elementos componibles arbitrarios con modificadores
Puedes aplicar modificadores de gestos a cualquier elemento componible arbitrario para hacer que la
de elementos componibles escuchan gestos. Por ejemplo, puedes permitir que un objeto Box
genérico
controlar los gestos de presión haciéndolo clickable
o dejar que un Column
controlar el desplazamiento vertical aplicando verticalScroll
Hay muchos modificadores para controlar diferentes tipos de gestos:
- Controla las presiones y las presiones con el
clickable
,combinedClickable
,selectable
,toggleable
ytriStateToggleable
. - Controla el desplazamiento con
horizontalScroll
.verticalScroll
y modificadoresscrollable
más genéricos. - Controla el arrastre con
draggable
yswipeable
. modificador. - Controlar gestos de varios toques, como desplazamiento lateral, rotación y zoom, con
el modificador
transformable
.
Como regla general, prioriza los modificadores de gestos listos para usar en lugar del control gestual personalizado.
Los modificadores agregan más funcionalidad al control de eventos de puntero puro.
Por ejemplo, el modificador clickable
no solo agrega detección de presiones y
pero también agrega información semántica,
indicaciones visuales a las interacciones,
desplazamiento, enfoque y compatibilidad con el teclado. Puedes verificar el código fuente
de clickable
para ver cómo la funcionalidad
se está agregando.
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. Para
Por ejemplo, no puedes usar un modificador para reaccionar a un arrastre después de mantener presionado un elemento
haz clic con la tecla Ctrl presionada o presiona con tres dedos. En cambio, puedes escribir tu propio gesto
para identificar estos gestos personalizados. Puedes crear un controlador de gestos con
El modificador pointerInput
, que te da acceso al puntero sin procesar
eventos.
El siguiente código escucha 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
Puedes pasarle una o más claves. Cuando valor de una de esas teclas cambia, el contenido lambda del modificador es se vuelven a ejecutar. La muestra pasa un filtro opcional al elemento componible. Si cambia el valor de ese filtro, el controlador de eventos del puntero volver a ejecutarse para garantizar que se registren los eventos correctos. awaitPointerEventScope
crea un alcance de corrutinas que se puede usar para lo siguiente: los eventos del puntero.awaitPointerEvent
suspende la corrutina hasta un siguiente evento de puntero. de que ocurra.
Aunque escuchar eventos de entrada sin procesar es una herramienta poderosa, también es complejo escribir un gesto personalizado basado en estos datos sin procesar. Para simplificar la creación de modelos hay muchos métodos de utilidad disponibles.
Cómo detectar gestos completos
En lugar de controlar los eventos del puntero sin procesar, puedes escuchar gestos específicos
y responder de forma apropiada. AwaitPointerEventScope
proporciona
métodos para detectar lo siguiente:
- Presionar dos veces, presionar dos veces y mantener 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 uno
Modificador pointerInput
. El siguiente fragmento solo detecta los toques, no
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 el segundo
no se alcanza. Si necesitas agregar más de un objeto de escucha de gestos a
un elemento componible, usa instancias de modificador pointerInput
independientes en su lugar:
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 la
awaitEachGesture
en lugar del bucle while(true)
que
pasa por cada evento sin procesar. El método awaitEachGesture
reinicia la
un bloque contenedor cuando todos los punteros se han levantado, lo que indica que el gesto se
completada:
@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 sea
responder a eventos del puntero sin identificar 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.
Espera a un evento o subgesto específico
Hay un conjunto de métodos que ayuda a identificar las partes comunes de los gestos:
- Suspender hasta que un puntero baje con
awaitFirstDown
, o esperar a que todos punteros para avanzar conwaitForUpOrCancellation
. - Crea un objeto de escucha de arrastre de bajo nivel con
awaitTouchSlopOrCancellation
. yawaitDragOrCancellation
. El controlador de gestos se suspende primero El puntero alcanza la inclinación táctil y luego se suspende hasta que se produce un primer evento de arrastre. lo que ocurre. Si solo te interesan los arrastres a lo largo de un solo eje, usa Más deawaitHorizontalTouchSlopOrCancellation
awaitHorizontalDragOrCancellation
oawaitVerticalTouchSlopOrCancellation
másawaitVerticalDragOrCancellation
en su lugar. - Suspender hasta que se mantenga presionado con
awaitLongPressOrCancellation
. - Usa el método
drag
para escuchar eventos de arrastre de forma continua.horizontalDrag
overticalDrag
para escuchar eventos de arrastre en un dispositivo eje horizontal.
Aplica cálculos para eventos multitáctiles
Cuando un usuario realiza un gesto multitáctil 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 detectTransformGestures
no brindan un control detallado para tu caso de uso, puedes
escuchar los eventos sin procesar y aplicar cálculos a ellos. Estos métodos auxiliares
son calculateCentroid
, calculateCentroidSize
,
calculatePan
, calculateRotation
y calculateZoom
.
Envío de eventos y prueba de posicionamiento
No todos los eventos del puntero se envían a todos los modificadores pointerInput
. Evento
el envío funciona de la siguiente manera:
- Los eventos de puntero se envían a una jerarquía componible. El momento en que un puntero nuevo activa su primer evento de puntero, el sistema comienza la prueba de posicionamiento ser "apto", elementos componibles. Un elemento componible se considera apto cuando tiene y capacidades de manejo de entradas de puntero. Flujos de pruebas de posicionamiento desde la parte superior de la IU árbol en la parte inferior. Un elemento componible es "hit". cuándo ocurrió el evento del puntero dentro de los límites de ese elemento. Este proceso da como resultado una cadena de funciones de componibilidad que realizan pruebas de posicionamiento de manera positiva.
- De forma predeterminada, cuando hay varios elementos componibles aptos en el mismo nivel de
el árbol, solo el elemento componible con el índice z más alto es "hit". Para
Por ejemplo, cuando agregas dos elementos componibles
Button
superpuestos a unBox
, solo el que se dibuja en la parte superior recibe los eventos de puntero. Teóricamente, puedes anular este comportamiento creando tu propioPointerInputModifierNode
y configurasharePointerInputWithSiblings
como verdadero. - Se despachan más eventos para el mismo puntero a esa misma cadena de elementos componibles y fluyen según la lógica de propagación de eventos. El sistema ya no realiza más pruebas de posicionamiento para este puntero. Esto significa que cada elemento componible en la cadena recibe todos los eventos de ese puntero, incluso cuando que ocurren fuera de los límites de ese elemento componible. Elementos componibles que no son de la cadena nunca reciben eventos del puntero, incluso cuando el puntero dentro de sus límites.
Los eventos de colocar el cursor sobre un elemento, que se activan cuando se coloca el cursor sobre un elemento o una pluma stylus, son una excepción a la definidas aquí. Los eventos de colocar el cursor sobre un elemento se envían a cualquier elemento componible que presione. De esta manera, Cuando un usuario coloca el cursor sobre un puntero desde los límites de un elemento componible hasta el siguiente en lugar de enviar los eventos al primer elemento componible, estos se envían al nuevo elemento componible.
Consumo de eventos
Cuando más de un elemento componible tiene asignado un controlador de gestos, esos los controladores no deberían entrar en conflicto. Por ejemplo, observa esta IU:
Cuando un usuario presiona el botón de favoritos, la lambda onClick
del botón controla eso
gesto. Cuando un usuario presiona cualquier otra parte del elemento de la lista, ListItem
controla ese gesto y navega hasta el artículo. En cuanto a la entrada del puntero,
el elemento Button debe consumir este evento, de modo que su elemento superior sepa que no debe
reaccionan a ella más. Los gestos incluidos en los componentes listos para usar y el
los modificadores de gestos comunes incluyen este comportamiento de consumo, pero si
cuando escribes tu propio gesto personalizado, debes consumir los eventos de forma manual. Tú lo haces
Con 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. R el elemento componible debe ignorar de forma explícita los eventos consumidos. Al escribir gestos personalizados, debes comprobar si un evento ya fue consumido por otro elemento:
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 del puntero se pasan a cada elemento componible que alcanza.
Pero si existe más de uno de esos elementos componibles, ¿en qué orden los eventos
propagarse? Si tomas el ejemplo de la última sección, esta IU se traduce a
el siguiente árbol de IU, en el que solo ListItem
y Button
responden a
eventos del puntero:
Los eventos de puntero fluyen por cada uno de estos elementos componibles tres veces durante tres "pases":
- En el Paso inicial, el evento fluye desde la parte superior del árbol de IU hasta la
abajo. Este flujo permite que un elemento superior intercepte un evento antes de que el elemento secundario
y consumirlo. Por ejemplo, la información sobre la herramienta debe interceptar un
mantener presionado en lugar de enviárselo a sus hijos. En nuestra
Por ejemplo,
ListItem
recibe el evento antes deButton
. - En el pase principal, el evento fluye desde los nodos de hoja del árbol de IU hasta los
raíz del árbol de IU. En esta fase, normalmente se consumen gestos, y es
el pase predeterminado cuando se escuchan eventos. Cómo controlar los gestos en este pase
significa que los nodos hoja tienen prioridad sobre los nodos superiores, que es el
el comportamiento más lógico para 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 de la IU. árbol a los nodos hoja. Este flujo permite que los elementos que están más arriba en la pila de respuesta ante el consumo de eventos por parte de su madre o padre. Por ejemplo, un botón quita su indicación de ondas cuando una presió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, esta 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 uno de estas esperan llamadas de método, aunque los datos sobre el consumo pueden tener cambió.
Probar gestos
En tus métodos de prueba, puedes enviar manualmente eventos del puntero con el
performTouchInput
. Esto te permite realizar análisis de
gestos completos (como pellizcar o hacer clic largo) o gestos de bajo nivel (como
mover el cursor a una determinada cantidad de píxeles):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Consulta la documentación de performTouchInput
para obtener más ejemplos.
Más información
Puedes obtener más información sobre los gestos en Jetpack Compose a través de los siguientes vínculos: recursos:
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Accesibilidad en Compose
- Desplazamiento
- Presionar y presionar