Funzionalità avanzate dello stilo

Android e ChromeOS forniscono una serie di API per aiutarti a creare app che offrono agli utenti un'esperienza stilo eccezionale. Il corso MotionEvent espone informazioni sull'interazione dello stilo con lo schermo, tra cui pressione, orientamento, inclinazione, passaggio del mouse e rilevamento del palmo dello stilo. Le librerie di previsione dei movimenti e della grafica a bassa latenza migliorano il rendering con lo stilo sullo schermo per offrire un'esperienza naturale, simile a penna e carta.

MotionEvent

La classe MotionEvent rappresenta le interazioni di input dell'utente, come la posizione e il movimento dei puntatori touch sullo schermo. Per l'input con lo stilo, MotionEvent espone anche i dati su pressione, orientamento, inclinazione e passaggio del mouse.

Dati sugli eventi

Per accedere ai dati di MotionEvent, aggiungi un modificatore pointerInput ai componenti:

@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 oggetto MotionEvent fornisce dati relativi ai seguenti aspetti di un evento UI:

  • Azioni: interazione fisica con il dispositivo, ovvero toccare lo schermo, spostare un puntatore sulla superficie dello schermo, passare un puntatore sulla superficie dello schermo
  • Puntatori: identificatori degli oggetti che interagiscono con lo schermo: dita, stilo, mouse
  • Asse: tipo di dati: coordinate x e y, pressione, inclinazione, orientamento e passaggio del mouse (distanza)

Azioni

Per implementare il supporto dello stilo, devi capire quale azione sta compiendo l'utente.

MotionEvent fornisce un'ampia varietà di costanti ACTION che definiscono gli eventi di movimento. Le azioni più importanti per lo stilo includono:

Azione Descrizione
AZIONE_GIÙ
AZIONE_POINTER_GIÙ
Il puntatore ha stabilito un contatto con lo schermo.
SPOSTA_AZIONE Il puntatore si sta muovendo sullo schermo.
ACTION_UP
ACTION_POINTER_UP
Il puntatore non è più in contatto con lo schermo
ANNULLA AZIONE Quando il movimento precedente o corrente deve essere annullato.

La tua app può eseguire attività come iniziare un nuovo tratto quando ACTION_DOWN si verifica, tracciare il tratto con ACTION_MOVE, e terminarlo quando ACTION_UP viene attivato.

L'insieme di azioni MotionEvent da ACTION_DOWN a ACTION_UP per un determinato pointer è chiamato set di movimenti.

Puntatori

La maggior parte delle schermate è multi-touch: il sistema assegna un puntatore a ogni dito, stilo, mouse o altro oggetto di punta che interagisce con lo schermo. Un indice del puntatore consente di ottenere le informazioni dell'asse per un puntatore specifico, ad esempio la posizione del primo dito che tocca lo schermo o del secondo.

Gli indici dei puntatori vanno da zero al numero di puntatori restituiti da MotionEvent#pointerCount() meno 1.

È possibile accedere ai valori dell'asse dei puntatori con il metodo getAxisValue(axis, pointerIndex). Quando l'indice del puntatore viene omesso, il sistema restituisce il valore per il primo puntatore, ovvero il puntatore zero (0).

Gli oggetti MotionEvent contengono informazioni sul tipo di puntatore in uso. Puoi ottenere il tipo di puntatore eseguendo l'iterazione attraverso gli indici dei cursori e chiamando il metodo getToolType(pointerIndex).

Per ulteriori informazioni sui puntatori, vedi Gestire i gesti multi-touch.

Input stilo

Puoi filtrare in base agli input dello stilo con TOOL_TYPE_STYLUS:

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

Lo stilo può anche segnalare che viene utilizzato come gomma con TOOL_TYPE_ERASER:

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

Dati asse stilo

ACTION_DOWN e ACTION_MOVE forniscono i dati degli assi relativi allo stilo, ovvero le coordinate x e y, la pressione, l'orientamento, l'inclinazione e il passaggio del mouse.

Per abilitare l'accesso a questi dati, l'API MotionEvent fornisce getAxisValue(int), in cui il parametro è uno dei seguenti identificatori dell'asse:

Axis Valore restituito di getAxisValue()
AXIS_X Coordinata X di un evento di movimento.
AXIS_Y Coordinata Y di un evento di movimento.
AXIS_PRESSURE Per un touchscreen o un touchpad, la pressione applicata da un dito, uno stilo o un altro puntatore. Per un mouse o una trackball, 1 se viene premuto il pulsante principale, 0 in caso contrario.
AXIS_ORIENTATION Per un touchscreen o un touchpad, l'orientamento di un dito, dello stilo o di un altro puntatore rispetto al piano verticale del dispositivo.
AXIS_TILT L'angolo di inclinazione dello stilo espresso in radianti.
AXIS_DISTANCE La distanza dello stilo dallo schermo.

Ad esempio, MotionEvent.getAxisValue(AXIS_X) restituisce la coordinata x per il primo puntatore.

Vedi anche Gestire i gesti multi-touch.

Posizione

Puoi recuperare le coordinate x e y di un puntatore con le seguenti chiamate:

Disegno con lo stilo sullo schermo con le coordinate X e Y mappate.
Figura 1. Coordinate dello schermo X e Y di un puntatore dello stilo.

Pressione

Puoi recuperare la pressione del puntatore con MotionEvent#getAxisValue(AXIS_PRESSURE) o, per il primo puntatore, MotionEvent#getPressure().

Il valore di pressione per i touchscreen o i touchpad è un valore compreso tra 0 (nessuna pressione) e 1, ma è possibile restituire valori più alti a seconda della calibrazione dello schermo.

Stilo che rappresenta un continuum di pressione da bassa ad alta. L'tratto è stretto e debole a sinistra, ad indicare una bassa pressione. Il tratto diventa più ampio e scuro da sinistra a destra fino a quando non diventa più largo e più scuro all'estrema destra, indicando la pressione più alta.
Figura 2. Rappresentazione della pressione: bassa pressione a sinistra, alta pressione a destra.

Orientamento

L'orientamento indica la direzione in cui è rivolto lo stilo.

L'orientamento del puntatore può essere recuperato utilizzando getAxisValue(AXIS_ORIENTATION) o getOrientation() (per il primo puntatore).

Per uno stilo, l'orientamento viene restituito come un valore in radianti compreso tra 0 e pi greco (p) in senso orario o da 0 a -pi greco in senso antiorario.

L'orientamento consente di implementare un pennello reale. Ad esempio, se lo stilo rappresenta un pennello piatto, la larghezza di quest'ultimo dipende dall'orientamento dello stilo.

Figura 3. Stilo che indica a sinistra circa meno 0,57 radianti.

Inclinazione

L'inclinazione misura l'inclinazione dello stilo rispetto allo schermo.

L'inclinazione restituisce l'angolo positivo dello stilo espresso in radianti, dove zero è perpendicolare allo schermo e p/2 è piatto sulla superficie.

L'angolo di inclinazione può essere recuperato utilizzando getAxisValue(AXIS_TILT) (nessuna scorciatoia per il primo puntatore).

L'inclinazione può essere utilizzata per riprodurre il più vicino possibile strumenti reali, come la simulazione di ombre con una matita inclinata.

Stilo inclinato di circa 40 gradi dalla superficie dello schermo.
Figura 4. Stilo inclinato di circa 0, 785 radianti o 45 gradi dalla perpendicolare.

Passaci il mouse sopra

La distanza dello stilo dallo schermo può essere ottenuta con getAxisValue(AXIS_DISTANCE). Il metodo restituisce un valore compreso tra 0,0 (contatto con lo schermo) a valori più alti quando lo stilo si allontana dallo schermo. La distanza tra lo schermo e il pennino (punto) dello stilo dipende dal produttore dello schermo e dello stilo. Poiché le implementazioni possono variare, non fare affidamento su valori precisi per le funzionalità fondamentali dell'app.

È possibile utilizzare il passaggio del mouse sullo stilo per visualizzare l'anteprima delle dimensioni del pennello o indicare che verrà selezionato un pulsante.

Figura 5. Stilo sopra uno schermo. L'app reagisce anche se lo stilo non tocca la superficie dello schermo.

Nota:Compose fornisce modificatori che influiscono sullo stato interattivo degli elementi dell'interfaccia utente:

  • hoverable: configura il componente per il passaggio del mouse utilizzando gli eventi di entrata e uscita del puntatore.
  • indication: consente di disegnare effetti visivi per questo componente quando si verificano delle interazioni.

Rifiuto del palmo della mano, navigazione e input indesiderati

A volte gli schermi multi-touch possono registrare tocchi indesiderati, ad esempio quando un utente appoggia naturalmente la mano sullo schermo per ricevere supporto durante la scrittura a mano libera. Il rifiuto del palmo della mano è un meccanismo che rileva questo comportamento e ti avvisa che l'ultima impostazione di MotionEvent deve essere annullata.

Di conseguenza, devi conservare una cronologia degli input utente in modo che i tocchi indesiderati possono essere rimossi dallo schermo e gli input utente legittimi possano essere renderizzati.

ACTION_CANCEL e FLAG_CANCELED

ACTION_CANCEL e FLAG_CANCELED sono entrambi progettati per informarti che il precedente set di MotionEvent deve essere annullato dall'ultimo ACTION_DOWN, in modo da poter, ad esempio, annullare l'ultimo tratto di un'app per disegnare per un determinato puntatore.

ANNULLA AZIONE

Aggiunte in Android 1.0 (livello API 1)

ACTION_CANCEL indica che la serie di eventi di movimento precedente deve essere annullata.

ACTION_CANCEL viene attivato quando viene rilevata una delle seguenti condizioni:

  • Gesti di navigazione
  • Rifiuto del palmo della mano

Quando ACTION_CANCEL viene attivato, devi identificare il puntatore attivo con getPointerId(getActionIndex()). Quindi rimuovi il tratto creato con quel puntatore dalla cronologia di input ed esegui nuovamente il rendering della scena.

SEGNALAZIONE_ANNULLATA

Aggiunte in Android 13 (livello API 33)

FLAG_CANCELED indica che il puntatore alzato era un tocco involontario dell'utente. Il flag in genere viene impostato quando l'utente tocca accidentalmente lo schermo, ad esempio afferrando il dispositivo o posizionando il palmo della mano sullo schermo.

Per accedere al valore del flag:

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

Se il flag è impostato, devi annullare l'ultima impostazione di MotionEvent, dall'ultima ACTION_DOWN da questo puntatore.

Come ACTION_CANCEL, il puntatore si trova con getPointerId(actionIndex).

Figura 6. Il tocco con lo stilo e il tocco con il palmo creano dei set di MotionEvent. Il palmo della mano è stato annullato e il display viene sottoposto di nuovo a rendering.

Gesti a schermo intero, edge-to-edge e di navigazione

Se un'app è a schermo intero e ha elementi interattivi vicino al bordo, come la tela di un'app per disegnare o per prendere appunti, far scorrere la schermata per visualizzare la navigazione o spostare l'app sullo sfondo potrebbe causare un tocco indesiderato.

Figura 7. Gesto di scorrimento per spostare un'app in background.

Per evitare che i gesti attivino tocchi indesiderati nella tua app, puoi sfruttare i inserti e ACTION_CANCEL.

Consulta anche la sezione Rifiuto, navigazione e input indesiderati di Palm.

Usa il metodo setSystemBarsBehavior() e BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE di WindowInsetsController per evitare che i gesti di navigazione provochino eventi di tocco indesiderati:

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

Per scoprire di più sulla gestione dei riquadri e dei gesti, vedi:

Bassa latenza

La latenza è il tempo richiesto dall'hardware, dal sistema e dall'applicazione per elaborare e visualizzare l'input dell'utente.

Latenza = elaborazione input hardware e sistema operativo + elaborazione app + compositing del sistema

  • rendering hardware
La latenza causa un ritardo rispetto alla posizione dello stilo per il tratto sottoposto a rendering. Lo spazio tra il tratto sottoposto a rendering e la posizione dello stilo rappresenta la latenza.
Figura 8. La latenza causa un ritardo rispetto alla posizione dello stilo per il tratto sottoposto a rendering.

Origine della latenza

  • Registrazione stilo con touchscreen (hardware): connessione wireless iniziale quando lo stilo e il sistema operativo comunicano per essere registrati e sincronizzati.
  • Frequenza di campionamento del tocco (hardware): il numero di volte al secondo per cui un touchscreen controlla se un puntatore tocca la superficie, compreso tra 60 e 1000 Hz.
  • Elaborazione dell'input (app): applicazione di colore, effetti grafici e trasformazione all'input dell'utente.
  • Rendering grafico (sistema operativo + hardware): scambio del buffer, elaborazione hardware.

Grafica a bassa latenza

La libreria grafica a bassa latenza di Jetpack riduce i tempi di elaborazione tra l'input dell'utente e il rendering sullo schermo.

La libreria riduce i tempi di elaborazione evitando il rendering multi-buffer e sfruttando una tecnica di rendering con buffer frontale, che significa scrivere direttamente sullo schermo.

Rendering buffer frontale

Il buffer anteriore è la memoria utilizzata dallo schermo per il rendering. Sono le app più vicine che riescono ad attirare direttamente sullo schermo. La libreria a bassa latenza consente alle app di eseguire il rendering direttamente nel buffer frontale. Ciò migliora le prestazioni impedendo lo scambio del buffer, che può verificarsi nel normale rendering multi-buffer o nel rendering doppio buffer (il caso più comune).

L'app scrive nel buffer dello schermo e legge dal buffer.
Figura 9. Rendering del buffer frontale.
L'app scrive sul buffer multi-buffer, che viene scambiato con il buffer dello schermo. L'app legge dal buffer dello schermo.
Figura 10. Rendering multi-buffer.

Sebbene il rendering con buffer frontale sia un'ottima tecnica per eseguire il rendering di una piccola area dello schermo, non è progettato per essere utilizzato per aggiornare l'intero schermo. Con il rendering con buffer frontale, l'app esegue il rendering dei contenuti in un buffer da cui il display sta leggendo. Di conseguenza, esiste la possibilità di visualizzare artefatti o tealing (vedi sotto).

La libreria a bassa latenza è disponibile a partire da Android 10 (livello API 29) e versioni successive e sui dispositivi ChromeOS con Android 10 (livello API 29) e versioni successive.

Dipendenze

La libreria a bassa latenza fornisce i componenti per l'implementazione del rendering del buffer frontale. La libreria viene aggiunta come dipendenza nel file build.gradle del modulo dell'app:

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

Callback di GLFrontBufferRenderer

La libreria a bassa latenza include l'interfaccia GLFrontBufferRenderer.Callback, che definisce i seguenti metodi:

La libreria a bassa latenza non ha segreti per il tipo di dati che utilizzi con GLFrontBufferRenderer.

Tuttavia, la libreria elabora i dati come un flusso di centinaia di punti dati, quindi progetta i dati in modo da ottimizzare l'utilizzo e l'allocazione della memoria.

Callback

Per attivare i callback di rendering, implementa GLFrontBufferedRenderer.Callback ed esegui l'override di onDrawFrontBufferedLayer() e onDrawDoubleBufferedLayer(). GLFrontBufferedRenderer utilizza i callback per visualizzare i dati nel modo più ottimizzato possibile.

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.
   }
}
Dichiara un'istanza di GLFrontBufferedRenderer

Prepara GLFrontBufferedRenderer fornendo il SurfaceView e i callback che hai creato in precedenza. GLFrontBufferedRenderer ottimizza il rendering nella parte anteriore e nel doppio buffer usando i callback:

var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Rendering

Il rendering del buffer frontale inizia quando chiami il metodo renderFrontBufferedLayer(), che attiva il callback onDrawFrontBufferedLayer().

Il rendering a doppio buffer riprende quando chiami la funzione commit(), che attiva il callback onDrawMultiDoubleBufferedLayer().

Nell'esempio che segue, il processo viene eseguito nel buffer anteriore (rendering rapido) quando l'utente inizia a disegnare sullo schermo (ACTION_DOWN) e sposta il puntatore (ACTION_MOVE). Il processo viene eseguito nel doppio buffer quando il puntatore lascia la superficie dello schermo (ACTION_UP).

Puoi utilizzare requestUnbufferedDispatch() per chiedere che il sistema di input non raggruppa gli eventi di movimento in batch, ma li invii non appena sono disponibili:

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()
   }
}

Cosa fare e cosa non fare durante il rendering

Cosa fare

Piccole parti dello schermo, scrittura a mano libera, disegno o schizzi.

Cosa non fare

Aggiornamento a schermo intero, panoramica, zoom. Può causare strappi.

Strappo

Lo strappo si verifica quando lo schermo si aggiorna mentre il buffer dello schermo viene modificato. Una parte dello schermo mostra i nuovi dati, mentre un'altra mostra quelli vecchi.

Le parti superiore e inferiore dell&#39;immagine Android sono disallineate a causa dello strappo durante l&#39;aggiornamento dello schermo.
Figura 11. Strappo quando lo schermo viene aggiornato dall'alto verso il basso.

Previsione del movimento

La libreria di previsione del movimento di Jetpack riduce la latenza percepita stimando il percorso dell'utente e fornendo punti artificiali temporanei al renderer.

La libreria di previsione dei movimenti riceve input utente reali come oggetti MotionEvent. Gli oggetti contengono informazioni su coordinate x e y, pressione e tempo, sfruttate dal predittore di movimento per prevedere oggetti MotionEvent futuro.

Gli oggetti MotionEvent previsti sono solo stime. Gli eventi previsti possono ridurre la latenza percepita, ma i dati previsti devono essere sostituiti con dati MotionEvent effettivi una volta ricevuti.

La libreria di previsione dei movimenti è disponibile a partire da Android 4.4 (livello API 19) e versioni successive e sui dispositivi ChromeOS con Android 9 (livello API 28) e versioni successive.

La latenza causa un ritardo rispetto alla posizione dello stilo per il tratto sottoposto a rendering. Lo spazio tra il tratto e lo stilo è pieno di punti di previsione. Il divario rimanente è la latenza percepita.
Figura 12. Latenza ridotta dalla previsione del movimento.

Dipendenze

La libreria di previsione del movimento fornisce l'implementazione della previsione. La libreria viene aggiunta come dipendenza nel file del modulo build.gradle dell'app:

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

Implementazione

La libreria di previsione del movimento include l'interfaccia MotionEventPredictor, che definisce i seguenti metodi:

  • record(): memorizza MotionEvent oggetti come record delle azioni dell'utente
  • predict(): restituisce un valore MotionEvent previsto
Dichiara un'istanza di MotionEventPredictor
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Fornisci dati al predittore
motionEventPredictor.record(motionEvent)
Previsione

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

Cosa fare e cosa non fare con la previsione del movimento

Cosa fare

Rimuovi i punti di previsione quando viene aggiunto un nuovo punto previsto.

Cosa non fare

Non utilizzare punti di previsione per il rendering finale.

App per creare note

ChromeOS consente alla tua app di dichiarare alcune azioni per prendere appunti.

Per registrare un'app come app per creare note su ChromeOS, consulta Compatibilità degli input.

Per registrare un'app come app per scrivere note su Android, vedi Creare un'app per scrivere note.

Android 14 (livello API 34), ha introdotto l'intent ACTION_CREATE_NOTE, che consente alla tua app di avviare un'attività per creare note sulla schermata di blocco.

Riconoscimento dell'inchiostro digitale con ML Kit

Con il riconoscimento a inchiostro digitale del kit ML, la tua app può riconoscere il testo scritto a mano su una piattaforma digitale in centinaia di lingue. Puoi anche classificare gli schizzi.

ML Kit fornisce la classe Ink.Stroke.Builder per creare oggetti Ink che possono essere elaborati da modelli di machine learning per convertire la scrittura a mano libera in testo.

Oltre al riconoscimento della scrittura a mano libera, il modello è in grado di riconoscere i gesti, come elimina e cerchia.

Per saperne di più, consulta Riconoscimento dell'inchiostro digitale.

Risorse aggiuntive

Guide per gli sviluppatori

Codelab