Comprende los gestos

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:

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:

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:

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 un Box, solo el que se dibuja en la parte superior recibe los eventos de puntero. Teóricamente, puedes anular este comportamiento creando tu propio PointerInputModifierNode y configura sharePointerInputWithSiblings 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:

Elemento de lista con una imagen, una columna con dos textos y un botón.

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:

Estructura de árbol. La capa superior es ListItem, la segunda tiene Image, Column y Button, y Column se divide en dos Texts. ListItem y Button están destacados.

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 de Button.
  • 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 de ListItem.
  • 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: