Funciones avanzadas de la pluma stylus

Android y ChromeOS proporcionan una variedad de APIs para ayudarte a compilar apps que ofrecen a los usuarios una experiencia excepcional con la pluma stylus. El La clase MotionEvent expone información sobre la interacción de la pluma stylus con la pantalla, incluida su presión detección de orientación, inclinación, desplazamiento y palma. Gráficos y movimiento de baja latencia las bibliotecas de predicción mejoran la renderización en pantalla de la pluma stylus para proporcionar un natural, similar a la del lápiz y el papel.

MotionEvent

La clase MotionEvent representa las interacciones de entrada del usuario, como la posición. y el movimiento de los punteros táctiles en la pantalla. Para entrada de pluma stylus, MotionEvent también expone datos de presión, orientación, inclinación y desplazamiento.

Datos de eventos

To access MotionEvent data, add a pointerInput modifier to components:

@Composable
fun Greeting() {
    Text(
        text = "Hello, Android!", textAlign = TextAlign.Center, style = TextStyle(fontSize = 5.em),
        modifier = Modifier
            .pointerInput(Unit) {
                awaitEachGesture {
                    while (true) {
                        val event = awaitPointerEvent()
                        event.changes.forEach { println(it) }
                    }
                }
            },
    )
}

Un objeto MotionEvent proporciona datos relacionados con los siguientes aspectos de una IU evento:

  • Acciones: interacción física con el dispositivo (tocar la pantalla, mover un puntero sobre la superficie de la pantalla, colocar el puntero sobre la pantalla superficie
  • Punteros: identificadores de objetos que interactúan con la pantalla (dedo, pluma stylus, mouse
  • Eje: tipo de datos (coordenadas x e y, presión, inclinación, orientación, y desplazamiento (distancia)

Acciones

Para implementar la compatibilidad con la pluma stylus, debes comprender qué acción realiza el usuario rendimiento.

MotionEvent proporciona una amplia variedad de constantes ACTION que definen el movimiento. eventos. Algunas de las acciones más importantes de la pluma stylus son las siguientes:

Acción Descripción
ACTION_DOWN
ACTION_POINTER_DOWN
El puntero entró en contacto con la pantalla.
ACTION_MOVE El puntero se está moviendo en la pantalla.
ACTION_UP
ACTION_POINTER_UP
El puntero ya no está en contacto con la pantalla
ACTION_CANCEL Cuándo se debe cancelar el conjunto de movimientos anterior o actual.

Tu app puede realizar tareas, como iniciar un nuevo trazo, cuando ACTION_DOWN dibujar el trazo con ACTION_MOVE, y finalizarlo cuando Se activó ACTION_UP.

El conjunto de acciones MotionEvent de ACTION_DOWN a ACTION_UP para un determinado se denomina conjunto de movimientos.

Punteros

La mayoría de las pantallas son multitáctiles: el sistema asigna un puntero para cada dedo, pluma stylus, mouse u otro objeto apuntando que interactúa con la pantalla. Un puntero índice te permite obtener información sobre los ejes de un puntero específico, como el posición del primer dedo que toca la pantalla o el segundo.

Los índices del puntero van de cero a la cantidad de punteros que muestra MotionEvent#pointerCount() menos 1.

Se puede acceder a los valores de los ejes de los punteros con el método getAxisValue(axis, pointerIndex). Cuando se omite el índice del puntero, el sistema muestra el valor del primer puntero, puntero cero (0).

Los objetos MotionEvent contienen información sobre el tipo de puntero que se usa. Tú puedes obtener el tipo de puntero iterando a través de los índices del puntero y llamando el getToolType(pointerIndex) .

Para obtener más información sobre los punteros, consulta Cómo controlar la función multitáctil gestos.

Entradas de la pluma stylus

Puedes filtrar las entradas de la pluma stylus con TOOL_TYPE_STYLUS:

val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)

La pluma stylus también puede informar que se usa como borrador con TOOL_TYPE_ERASER:

val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)

Datos del eje de la pluma stylus

ACTION_DOWN y ACTION_MOVE proporcionan datos del eje sobre la pluma stylus, es decir, x y coordenadas y, presión, orientación, inclinación y desplazamiento.

Para habilitar el acceso a estos datos, la API de MotionEvent proporciona getAxisValue(int), donde el parámetro es cualquiera de los siguientes identificadores del eje:

Axis Valor que se muestra de getAxisValue()
AXIS_X Coordenada X de un evento de movimiento.
AXIS_Y Coordenada Y de un evento de movimiento.
AXIS_PRESSURE Para una pantalla táctil o un panel táctil, la presión que se aplica con un dedo, una pluma stylus u otro puntero. Para un mouse o una bola de seguimiento, 1 si se presiona el botón principal, 0 de lo contrario.
AXIS_ORIENTATION En el caso de una pantalla táctil o un panel táctil, la orientación de un dedo, una pluma stylus u otro puntero en relación con el plano vertical del dispositivo.
AXIS_TILT El ángulo de inclinación de la pluma stylus en radianes.
AXIS_DISTANCE La distancia de la pluma stylus a la pantalla.

Por ejemplo, MotionEvent.getAxisValue(AXIS_X) devuelve la coordenada x para la primer puntero.

Consulta también Cómo controlar la función multitáctil gestos.

Posición

Puedes recuperar las coordenadas x e y de un puntero con las siguientes llamadas:

Dibujo de una pluma stylus en la pantalla con coordenadas x e y.
Figura 1: Coordenadas de pantalla x e y de un puntero de pluma stylus.

Presionar

Puedes recuperar la presión del puntero con MotionEvent#getAxisValue(AXIS_PRESSURE) o, para el primer puntero, MotionEvent#getPressure()

El valor de presión de las pantallas táctiles o los paneles táctiles es un valor entre 0 (sin presión) y 1, pero se pueden obtener valores más altos según la pantalla calibración.

Trazo de la pluma stylus que representa una continuidad de presión baja a alta. El trazo es angosto y débil a la izquierda, lo que indica presión baja. El trazo se hace más ancho y más oscuro de izquierda a derecha hasta que queda más ancho y más oscuro en el extremo derecho, lo que indica presión más alta.
Figura 2: Representación de presión: presión baja a la izquierda, presión alta a la derecha.

Orientación

La orientación indica hacia qué dirección apunta la pluma stylus.

La orientación del puntero se puede recuperar con getAxisValue(AXIS_ORIENTATION). getOrientation() (para el primer puntero).

En una pluma stylus, la orientación se muestra como un valor de radianes entre 0 y pi (π) en el sentido de las manecillas del reloj o 0 a -pi en sentido antihorario.

La orientación te permite implementar un pincel realista. Por ejemplo, si el representa un pincel plano, el ancho del pincel plano depende del orientación de la pluma stylus.

Figura 3: Pluma stylus que apunta hacia la izquierda alrededor de menos 0.57 radianes.

Inclinación

Esta función mide la inclinación de la pluma stylus en relación con la pantalla.

La inclinación muestra el ángulo positivo de la pluma stylus en radianes, donde cero es perpendicular a la pantalla y π/2 es plano sobre la superficie.

El ángulo de inclinación se puede recuperar con getAxisValue(AXIS_TILT) (sin combinación de teclas para el primer puntero).

La inclinación se puede usar para reproducir herramientas realistas de la forma más precisa posible, como imitando el sombreado con un lápiz inclinado.

Pluma stylus inclinada a aproximadamente 40 grados respecto de la superficie de la pantalla.
Figura 4: Pluma stylus inclinada a aproximadamente 0.785 radianes o 45 grados respecto de la perpendicular.

Colocar el cursor sobre un elemento

La distancia de la pluma stylus a la pantalla se puede obtener con getAxisValue(AXIS_DISTANCE) El método devuelve un valor de 0.0 (contacto con la pantalla) a valores más altos a medida que la pluma stylus se aleja de la pantalla. El botón de desplazamiento del mouse la distancia entre la pantalla y la punta de la pluma stylus depende del fabricante de la pantalla y de la pluma stylus. Debido a que las implementaciones pueden no dependan de valores precisos para funcionalidades críticas de la app.

El desplazamiento de la pluma stylus se puede usar para obtener una vista previa del tamaño del pincel o indicar que se se seleccionará el botón.

Figura 5: Pluma stylus sobre una pantalla. La app reacciona aunque la pluma stylus no toque la superficie de la pantalla.

Nota: Compose proporciona modificadores que afectan el estado interactivo de los elementos de la IU:

  • hoverable: Configura este componente para que se pueda colocar el cursor sobre él con los eventos de entrada y salida del puntero.
  • indication: Dibuja efectos visuales para este componente cuando se producen interacciones.

Rechazo de la palma, navegación y entradas no deseadas

A veces, las pantallas multitáctiles pueden registrar toques no deseados, por ejemplo, cuando un el usuario apoya naturalmente la mano en la pantalla para descansar mientras escribe. El rechazo de la palma es un mecanismo que detecta este comportamiento y te notifica que se debe cancelar el último conjunto de MotionEvent.

Por ello, debes mantener un historial de entradas del usuario para que los toques no deseados de la pantalla y las entradas legítimas de los usuarios se volvió a renderizar.

ACTION_CANCEL y FLAG_CANCELED

ACTION_CANCEL y FLAG_CANCELED son diseñados para informarte que el conjunto de MotionEvent anterior debe cancelados del último ACTION_DOWN para que puedas, por ejemplo, deshacer trazo de una app de dibujo para un puntero determinado.

ACTION_CANCEL

Se agregó en Android 1.0 (nivel de API 1)

ACTION_CANCEL indica que se debe cancelar el conjunto anterior de eventos de movimiento.

ACTION_CANCEL se activa cuando se detecta alguna de las siguientes opciones:

  • Gestos de navegación
  • Rechazo de la palma

Cuando se activa ACTION_CANCEL, debes identificar el puntero activo con getPointerId(getActionIndex()) Luego, quita el trazo creado con ese puntero del historial de entradas y vuelve a renderizar la escena.

FLAG_CANCELED

Se agregó en Android 13 (nivel de API 33)

FLAG_CANCELED indica que el movimiento del puntero hacia arriba fue un toque no intencional del usuario. La bandera es normalmente se configura cuando el usuario toca la pantalla accidentalmente, por ejemplo, al agarrar el dispositivo o colocando la palma de la mano sobre la pantalla.

Puedes acceder al valor de la marca de la siguiente manera:

val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED

Si la marca está establecida, debes deshacer el último conjunto de MotionEvent del último ACTION_DOWN de este puntero.

Al igual que ACTION_CANCEL, el puntero se puede encontrar con getPointerId(actionIndex).

Figura 6: El trazo de la pluma stylus y el toque de la palma crean conjuntos de MotionEvent. Se canceló el toque de la palma y se volvió a renderizar la pantalla.

Gestos de pantalla completa, de borde a borde y de navegación

Si una aplicación está en pantalla completa y tiene elementos accionables cerca del perímetro, como el lienzo de una app de dibujo o toma de notas, desliza el dedo desde la parte inferior de la pantalla para mostrar la navegación o mover la app a segundo plano podría generar una toque no deseado en el lienzo.

Figura 7: Gesto de deslizar el dedo para mover una app al segundo plano.

Para evitar que los gestos activen toques no deseados en tu app, puedes tomar aprovechar las inserciones y ACTION_CANCEL

Consulta también el artículo Rechazo de la palma, navegación y entradas no deseadas sección.

Usa el setSystemBarsBehavior() método y BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE de WindowInsetsController Para evitar que los gestos de navegación causen eventos táctiles no deseados, haz lo siguiente:

// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

Para obtener más información sobre la administración de inserciones y gestos, consulta:

Latencia baja

La latencia es el tiempo que requieren el hardware, el sistema y la aplicación para procesar y procesar la entrada del usuario.

Latencia = hardware y procesamiento de entrada del SO + procesamiento de la app + composición del sistema

  • renderización de hardware
La latencia hace que el trazo renderizado se retrase con respecto a la posición de la pluma stylus. El espacio entre el trazo renderizado y la posición de la pluma stylus representa la latencia.
Figura 8: La latencia hace que el trazo renderizado se retrase con respecto a la posición de la pluma stylus.

Fuente de latencia

  • Registro de la pluma stylus con pantalla táctil (hardware): Conexión inalámbrica inicial Cuando la pluma stylus y el SO se comunican para registrarse y sincronizarse
  • Tasa de muestreo táctil (hardware): Es la cantidad de veces por segundo que una pantalla táctil. comprueba si un puntero toca la superficie, con un rango de 60 a 1000 Hz.
  • Procesamiento de entrada (app): Aplicación de color, efectos gráficos y transformación según la entrada del usuario.
  • Renderización gráfica (SO + hardware): Cambio de búfer, procesamiento de hardware.

Gráficos de baja latencia

La biblioteca de gráficos de baja latencia de Jetpack Reduce el tiempo de procesamiento entre la entrada del usuario y la renderización en pantalla.

La biblioteca reduce el tiempo de procesamiento, ya que evita la renderización en varios búferes y Aprovecha una técnica de renderización en el búfer frontal, lo que significa escribir directamente en la pantalla.

Renderización del búfer frontal

El búfer frontal es la memoria que usa la pantalla para la renderización. Es el más cercano las apps pueden llegar a dibujar directamente en la pantalla. La biblioteca de latencia baja permite que las apps se rendericen directamente en el búfer frontal. Esto mejora el rendimiento Evitar el intercambio de búfer, que puede ocurrir en la renderización normal de búfer múltiple o en búfer doble (el caso más común).

La app escribe en el búfer de pantalla y lee desde él.
Figura 9: Renderización del búfer frontal.
La app escribe en un búfer múltiple, que se intercala con el búfer de pantalla. La app lee del búfer de pantalla.
Figura 10: Renderización de búfer múltiple.

Si bien la renderización del búfer frontal es una excelente técnica para renderizar un área pequeña de la pantalla, no está diseñada para usarse para actualizar toda la pantalla. Con la renderización del búfer frontal, la app renderiza contenido en un búfer desde el que está leyendo la pantalla. Como resultado, existe la posibilidad de renderizar artefactos o seccionamientos (ver a continuación).

La biblioteca de latencia baja está disponible a partir de Android 10 (nivel de API 29) y versiones posteriores y en dispositivos ChromeOS con Android 10 (nivel de API 29) y versiones posteriores.

Dependencias

La biblioteca de latencia baja proporciona los componentes para la renderización del búfer frontal para implementarlos. La biblioteca se agrega como una dependencia en el módulo de la app. Archivo build.gradle:

dependencies {
    implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}

Devoluciones de llamada de GLFrontBufferRenderer

La biblioteca de latencia baja incluye GLFrontBufferRenderer.Callback que define los siguientes métodos:

La biblioteca de baja latencia no se expresa sobre el tipo de datos que usas con GLFrontBufferRenderer

Sin embargo, la biblioteca procesa los datos como un flujo de cientos de datos. y diseñe sus datos para optimizar el uso y la asignación de memoria.

Devoluciones de llamada

Para habilitar las devoluciones de llamada de renderización, implementa GLFrontBufferedRenderer.Callback y se anulan onDrawFrontBufferedLayer() y onDrawDoubleBufferedLayer(). GLFrontBufferedRenderer usa las devoluciones de llamada para renderizar tus datos de la manera más manera optimizada posible.

val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> {
   override fun onDrawFrontBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       param: DATA_TYPE
   ) {
       // OpenGL for front buffer, short, affecting small area of the screen.
   }
   override fun onDrawMultiDoubleBufferedLayer(
       eglManager: EGLManager,
       bufferInfo: BufferInfo,
       transform: FloatArray,
       params: Collection<DATA_TYPE>
   ) {
       // OpenGL full scene rendering.
   }
}
Cómo declarar una instancia de GLFrontBufferedRenderer

Para preparar el GLFrontBufferedRenderer, proporciona SurfaceView y las devoluciones de llamada que creaste anteriormente. GLFrontBufferedRenderer optimiza la renderización al búfer frontal y doble mediante las devoluciones de llamada:

var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Renderización

La renderización del búfer frontal comienza cuando llamas al renderFrontBufferedLayer() que activa la devolución de llamada onDrawFrontBufferedLayer().

La renderización del búfer doble se reanuda cuando llamas al commit() función, que activa la devolución de llamada onDrawMultiDoubleBufferedLayer().

En el siguiente ejemplo, el proceso se renderiza en el búfer frontal (rápido renderización) cuando el usuario comienza a dibujar en la pantalla (ACTION_DOWN) y se mueve. el puntero alrededor (ACTION_MOVE). El proceso se renderiza en el búfer doble cuando el puntero sale de la superficie de la pantalla (ACTION_UP).

Puedes usar requestUnbufferedDispatch() solicitar que el sistema de entrada no agrupe los eventos de movimiento, sino que entregue en cuanto estén disponibles:

when (motionEvent.action) {
   MotionEvent.ACTION_DOWN -> {
       // Deliver input events as soon as they arrive.
       view.requestUnbufferedDispatch(motionEvent)
       // Pointer is in contact with the screen.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_MOVE -> {
       // Pointer is moving.
       glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE)
   }
   MotionEvent.ACTION_UP -> {
       // Pointer is not in contact in the screen.
       glFrontBufferRenderer.commit()
   }
   MotionEvent.CANCEL -> {
       // Cancel front buffer; remove last motion set from the screen.
       glFrontBufferRenderer.cancel()
   }
}

Sugerencias y precauciones para la renderización

✓ Acción

Puede utilizarse en pequeñas partes de la pantalla, escritura a mano, dibujo y esbozo.

✗ Evita las acciones

Actualización en pantalla completa, desplazamiento lateral y zoom. Puede provocar seccionamientos.

Seccionamientos

Los seccionamientos se producen cuando la pantalla se actualiza mientras se está cargando el búfer de pantalla modificar al mismo tiempo. Una parte de la pantalla muestra datos nuevos, mientras que otra muestra datos antiguos.

Las partes inferior y superior de la imagen de Android no están alineadas debido al seccionamiento cuando se actualiza la pantalla.
Figura 11: El seccionamiento se actualiza de la parte superior a la inferior.

Predicción del movimiento

La predicción de movimiento de Jetpack biblioteca reduce la latencia percibida al estimar la ruta del trazo del usuario y brindar información puntos artificiales al renderizador.

La biblioteca de predicción del movimiento obtiene entradas reales del usuario como objetos MotionEvent. Los objetos contienen información sobre las coordenadas x e y, la presión y el tiempo. que el predictor de movimiento aprovecha para predecir MotionEvent futuros. objetos.

Los objetos MotionEvent pronosticados son solo estimaciones. Los eventos predichos pueden reducir latencia percibida, pero los datos previstos deben reemplazarse por MotionEvent reales una vez recibidos.

La biblioteca de predicción del movimiento está disponible en Android 4.4 (nivel de API 19) y y en dispositivos ChromeOS con Android 9 (nivel de API 28) y versiones posteriores.

La latencia hace que el trazo renderizado se retrase con respecto a la posición de la pluma stylus. El espacio entre el trazo y la pluma stylus se llena con puntos de predicción. La brecha restante es la latencia percibida.
Figura 12: Se redujo la latencia mediante la predicción del movimiento.

Dependencias

La biblioteca de predicción del movimiento proporciona la implementación de la predicción. El Se agrega la biblioteca como una dependencia en el archivo build.gradle del módulo de la app:

dependencies {
    implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}

Implementación

La biblioteca de predicción del movimiento incluye MotionEventPredictor que define los siguientes métodos:

  • record(): Almacena objetos MotionEvent como un registro de las acciones del usuario.
  • predict(): Muestra un MotionEvent pronosticado.
Declara una instancia de MotionEventPredictor.
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Cómo alimentar el predictor con datos
motionEventPredictor.record(motionEvent)
Predecir

when (motionEvent.action) {
   MotionEvent.ACTION_MOVE -> {
       val predictedMotionEvent = motionEventPredictor?.predict()
       if(predictedMotionEvent != null) {
            // use predicted MotionEvent to inject a new artificial point
       }
   }
}

Sugerencias y precauciones para la predicción del movimiento

✓ Acción

Quita los puntos de predicción cuando se agregue un nuevo punto pronosticado.

✗ Evita las acciones

No uses puntos de predicción para la renderización final.

Apps para tomar notas

ChromeOS permite que tu app declare algunas acciones para tomar notas.

Si quieres registrar una app para tomar notas en ChromeOS, consulta Entrada compatibilidad.

Si quieres registrar una app para tomar notas en Android, consulta Cómo crear una toma de notas de la app.

Android 14 (nivel de API 34), presentó el ACTION_CREATE_NOTE intent, que permite que tu app inicie una actividad para tomar notas en la cerradura en la pantalla.

Reconocimiento de tinta digital con ML Kit

Con la tinta digital ML Kit reconocimiento de imágenes, tu aplicación puede reconocer texto escrito a mano en una superficie digital en cientos de idiomas. También puedes clasificar bocetos.

ML Kit proporciona la Ink.Stroke.Builder clase para crear objetos Ink que los modelos de aprendizaje automático pueden procesar para convertir la escritura a mano en texto.

Además del reconocimiento de escritura a mano, el modelo puede reconocer gestos, como borrar y encerrar en un círculo.

Consulta Tinta digital reconocimiento para obtener más información.

Recursos adicionales

Guías para desarrolladores

Codelabs