Android e ChromeOS forniscono una varietà di API per aiutarti a creare app che offrono agli utenti un'esperienza eccezionale con lo stilo. La classe MotionEvent
mostra informazioni sull'interazione dello stilo con lo schermo, inclusi pressione dello stilo, orientamento, inclinazione, passaggio del mouse e rilevamento del palmo. La grafica a bassa latenza e le librerie di previsione del movimento migliorano il rendering sullo schermo con lo stilo per offrire un'esperienza naturale, simile a carta e penna.
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 dello stilo, MotionEvent
espone anche i dati relativi a 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 della UI:
- Azioni: interazione fisica con il dispositivo, ad esempio tocco dello schermo, spostamento di un puntatore sulla superficie dello schermo e passaggio del puntatore sulla superficie dello schermo.
- Puntatori: identificatori degli oggetti che interagiscono con lo schermo (dito, stilo, mouse)
- Asse: tipo di dati, coordinate x e y, pressione, inclinazione, orientamento e passaggio del mouse (distanza)
Azioni
Per implementare il supporto con stilo, devi capire quale azione sta eseguendo l'utente.
MotionEvent
offre una vasta gamma di costanti ACTION
che definiscono gli eventi di movimento. Le azioni più importanti per lo stilo includono:
Azione | Descrizione |
---|---|
ACTION_DOWN ACTION_POINTER_DOWN |
Il puntatore è entrato in contatto con lo schermo. |
ACTION_MOVE | Il puntatore si muove sullo schermo. |
ACTION_UP ACTION_POINTER_UP |
Il puntatore non è più in contatto con lo schermo |
ACTION_ANNULLA | L'impostazione di movimento precedente o corrente dovrebbe essere annullata. |
La tua app può eseguire attività come iniziare un nuovo tratto quando si verifica ACTION_DOWN
, disegnare il tratto con ACTION_MOVE,
e completarlo quando viene attivato ACTION_UP
.
Il insieme di azioni MotionEvent
da ACTION_DOWN
a ACTION_UP
per un determinato puntatore è chiamato insieme di movimento.
Puntatori
La maggior parte delle schermate è multi-touch: il sistema assegna un puntatore a ogni dito, stilo, mouse o altro oggetto di puntamento che interagisce con lo schermo. Un indice che ti consente di ottenere le informazioni sull'asse di un puntatore specifico, ad esempio la posizione del primo dito che tocca lo schermo o del secondo.
Gli indici di puntatori sono compresi tra zero e il numero di puntatori restituiti da MotionEvent#pointerCount()
meno 1.
È possibile accedere ai valori dell'asse dei puntatori con il metodo getAxisValue(axis,
pointerIndex)
.
Se l'indice del puntatore viene omesso, il sistema restituisce il valore del 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 degli indici del puntatore e chiamando il
metodo
getToolType(pointerIndex)
.
Per scoprire di più sui puntatori, consulta la sezione Gestire i gesti multi-touch.
Input stilo
Puoi filtrare gli input dello stilo con
TOOL_TYPE_STYLUS
:
Kotlin
val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)
Java
boolean isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex);
Lo stilo può anche segnalare che è stato utilizzato come gomma con TOOL_TYPE_ERASER
:
Kotlin
val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)
Java
boolean isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex);
Dati dell'asse dello 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 consentire 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 touchpad, la pressione applicata da un dito, uno stilo o un altro puntatore. Per un mouse o una trackball, 1 se si preme il pulsante principale, 0 altrimenti. |
AXIS_ORIENTATION |
Per un touchscreen o 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 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:
MotionEvent#getAxisValue(AXIS_X)
oMotionEvent#getX()
MotionEvent#getAxisValue(AXIS_Y)
oMotionEvent#getY()
Pressione
Puoi recuperare la pressione del puntatore con
MotionEvent#getAxisValue(AXIS_PRESSURE)
o, come primo puntatore,
MotionEvent#getPressure()
.
Il valore della pressione per touchscreen o touchpad è compreso tra 0 (nessuna pressione) e 1, ma possono essere restituiti valori più elevati a seconda della calibrazione dello schermo.
Orientamento
L'orientamento indica la direzione in cui punta 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 sotto forma di valore radiante compreso tra 0 e pi greco (β) in senso orario o tra 0 e -pi 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.
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 strumenti reali il più ravvicinati possibile, come mimare l'ombreggiatura con una matita inclinata.
Passaci il mouse sopra
Puoi ottenere la distanza dello stilo dallo schermo con
getAxisValue(AXIS_DISTANCE)
. Il metodo restituisce un valore da 0,0 (contatto con lo schermo) a valori più alti quando lo stilo si allontana dallo schermo. La distanza
al passaggio del mouse tra lo schermo e il pennino (punto) dello stilo dipende dal
produttore sia dello schermo sia dello stilo. Poiché le implementazioni possono
variare, non fare affidamento su valori precisi per le funzionalità fondamentali per l'app.
Il passaggio del mouse sullo stilo può essere utilizzato per visualizzare l'anteprima delle dimensioni del pennello o per indicare che verrà selezionato un pulsante.
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
: traccia effetti visivi per questo componente quando si verificano interazioni.
Rifiuto del palmo, navigazione e input indesiderati
A volte gli schermi multi-touch possono registrare i tocchi indesiderati, ad esempio quando
un utente appoggia naturalmente la mano sullo schermo durante la scrittura a mano libera.
Il rifiuto del palmo è un meccanismo che rileva questo comportamento e ti informa che
l'ultima serie di MotionEvent
deve essere annullata.
Di conseguenza, devi conservare una cronologia degli input utente in modo che i tocchi indesiderati possano essere rimossi dallo schermo e che gli input legittimi dell'utente possano essere visualizzati di nuovo.
ACTION_CANCEL e FLAG_CANCELED
ACTION_CANCEL
e FLAG_CANCELED
sono entrambi progettati per informarti che il set MotionEvent
precedente deve essere annullato dall'ultimo ACTION_DOWN
, così puoi, ad esempio, annullare l'ultimo tratto di un'app di disegno per un determinato puntatore.
ACTION_ANNULLA
Aggiunta 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
Quando viene attivato ACTION_CANCEL
, 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
Aggiunta in Android 13 (livello API 33)
FLAG_CANCELED
indica che il cursore verso l'alto è stato toccato involontariamente dall'utente. In genere il flag 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:
Kotlin
val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED
Java
boolean cancel = (event.getFlags() & FLAG_CANCELED) == FLAG_CANCELED;
Se il flag è impostato, devi annullare l'ultimo MotionEvent
impostato, dall'ultimo
ACTION_DOWN
da questo puntatore.
Come ACTION_CANCEL
, il puntatore si trova con getPointerId(actionIndex)
.
Gesti a schermo intero, da bordo a bordo e di navigazione
Se un'app è a schermo intero e contiene elementi interattivi vicino al bordo, ad esempio sulla tela di un'app di disegno o per la creazione di note, se scorri dalla parte inferiore dello schermo per visualizzare la navigazione o sposta l'app sullo sfondo, potrebbe apparire un tocco indesiderato nella tela.
Per evitare che i gesti attivino tocchi indesiderati nella tua app, puoi sfruttare gli inserti e ACTION_CANCEL
.
Consulta anche la sezione Rifiuto di Palm, navigazione e input indesiderati.
Utilizza il metodo setSystemBarsBehavior()
e BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
di WindowInsetsController
per evitare che i gesti di navigazione provochino eventi touch indesiderati:
Kotlin
// Configure the behavior of the hidden system bars. windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
Java
// Configure the behavior of the hidden system bars. windowInsetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE );
Per scoprire di più sulla gestione di insiemi e gesti, consulta:
- Nascondi le barre di sistema per la modalità immersiva
- Assicurare la compatibilità con la navigazione tramite gesti
- Visualizzare i contenuti a livello perimetrale nell'app
Bassa latenza
La latenza è il tempo necessario all'hardware, al sistema e all'applicazione per elaborare e visualizzare l'input utente.
Latenza = elaborazione dell'input di hardware e sistema operativo + elaborazione di app + composizione del sistema
- rendering hardware
Origine della latenza
- Registrazione dello stilo con touchscreen (hardware): connessione wireless iniziale quando lo stilo e il sistema operativo comunicano per essere registrati e sincronizzati.
- Frequenza di campionamento al tocco (hardware): il numero di volte al secondo un touchscreen controlla se un puntatore tocca la superficie, nell'intervallo da 60 a 1000 Hz.
- Elaborazione input (app): applicazione di colore, effetti grafici e trasformazione all'input utente.
- Rendering grafico (sistema operativo + hardware): scambio del buffer, elaborazione hardware.
Grafica a bassa latenza
La libreria grafica a bassa latenza 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 del front-buffer, il che significa la scrittura direttamente sullo schermo.
Rendering front-buffer
Il buffer anteriore è la memoria utilizzata dallo schermo per il rendering. È l'app più vicina che può disegnare direttamente sullo schermo. La libreria a bassa latenza consente il rendering delle app direttamente nel buffer frontale. Questo migliora le prestazioni impedendo lo scambio del buffer, che può verificarsi per il rendering con multi-buffer regolare o per il rendering a doppio buffer (il caso più comune).
Sebbene il rendering del 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 del buffer frontale, l'app esegue il rendering dei contenuti in un buffer da cui il display sta leggendo. Di conseguenza, esiste la possibilità di eseguire il rendering degli artefatti o di lacerare (vedi di seguito).
La libreria a bassa latenza è disponibile a partire da Android 10 (livello API 29) 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 GLFrontBufferRenderer
La libreria a bassa latenza include l'interfaccia GLFrontBufferRenderer.Callback
, che definisce i seguenti metodi:
La libreria a bassa latenza non è adatta al tipo di dati che utilizzi con GLFrontBufferRenderer
.
Tuttavia, la libreria elabora i dati come un flusso di centinaia di punti dati e, quindi, progetta i tuoi dati per 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 tuoi dati nel modo più ottimizzato possibile.
Kotlin
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. } }
Java
GLFrontBufferedRenderer.Callback<DATA_TYPE> callbacks = new GLFrontBufferedRenderer.Callback<DATA_TYPE>() { @Override public void onDrawFrontBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, DATA_TYPE data_type) { // OpenGL for front buffer, short, affecting small area of the screen. } @Override public void onDrawDoubleBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, @NonNull Collection<? extends DATA_TYPE> collection) { // OpenGL full scene rendering. } };
Dichiara un'istanza di GLFrontBufferedRenderer
Prepara l'GLFrontBufferedRenderer
fornendo il valore SurfaceView
e
i callback che hai creato in precedenza. GLFrontBufferedRenderer
ottimizza il rendering
sul buffer anteriore e sul doppio buffer utilizzando i callback:
Kotlin
var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Java
GLFrontBufferedRenderer<DATA_TYPE> glFrontBufferRenderer = new GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks);
Rendering
Il rendering del buffer inizia quando chiami il metodo renderFrontBufferedLayer()
, che attiva il callback onDrawFrontBufferedLayer()
.
Il rendering del doppio buffer riprende quando chiami la funzione commit()
, che attiva il callback onDrawMultiDoubleBufferedLayer()
.
Nell'esempio che segue, il processo esegue il rendering nel buffer anteriore (rendering rapido) quando l'utente inizia a disegnare sullo schermo (ACTION_DOWN
) e sposta il puntatore intorno a (ACTION_MOVE
). Il processo esegue il rendering nel doppio buffer quando il puntatore esce dalla superficie dello schermo (ACTION_UP
).
Puoi utilizzare requestUnbufferedDispatch()
per chiedere che il sistema di input non batch gli eventi di movimento, ma li pubblichi non appena sono disponibili:
Kotlin
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() } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: { // Deliver input events as soon as they arrive. surfaceView.requestUnbufferedDispatch(motionEvent); // Pointer is in contact with the screen. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_MOVE: { // Pointer is moving. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_UP: { // Pointer is not in contact in the screen. glFrontBufferRenderer.commit(); } break; case MotionEvent.ACTION_CANCEL: { // Cancel front buffer; remove last motion set from the screen. glFrontBufferRenderer.cancel(); } break; }
Cosa fare e cosa non fare durante il rendering
Piccole parti dello schermo, scrittura a mano libera, disegno, schizzi.
Aggiornamento a schermo intero, panoramica e zoom. Possono causare danni.
Strappo
Il tearing si verifica quando lo schermo si aggiorna mentre il buffer dello schermo viene modificato contemporaneamente. Una parte dello schermo mostra nuovi dati, mentre un'altra i dati precedenti.
Previsione dei movimenti
La libreria di previsione del movimento Jetpack riduce la latenza percepita tramite una stima del percorso del tratto dell'utente e fornendo punti artificiali temporanei al renderer.
La libreria di previsioni del movimento riceve input utente reali come oggetti MotionEvent
.
Gli oggetti contengono informazioni sulle coordinate x e y, sulla pressione e sul tempo, che vengono sfruttate dal predittore di movimento per prevedere oggetti MotionEvent
futuri.
Gli oggetti MotionEvent
previsti sono solo stime. Gli eventi previsti possono ridurre la latenza percepita, ma i dati previsti devono essere sostituiti con i dati MotionEvent
effettivi una volta ricevuti.
La libreria di previsioni 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.
Dipendenze
La libreria di previsioni 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 previsioni del movimento include l'interfaccia MotionEventPredictor
, che definisce i seguenti metodi:
record()
: archiviaMotionEvent
oggetti come record delle azioni dell'utentepredict()
: restituisce un valoreMotionEvent
previsto
Dichiara un'istanza di MotionEventPredictor
Kotlin
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Java
MotionEventPredictor motionEventPredictor = MotionEventPredictor.newInstance(surfaceView);
Fornire dati al previsione
Kotlin
motionEventPredictor.record(motionEvent)
Java
motionEventPredictor.record(motionEvent);
Previsione
Kotlin
when (motionEvent.action) { MotionEvent.ACTION_MOVE -> { val predictedMotionEvent = motionEventPredictor?.predict() if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_MOVE: { MotionEvent predictedMotionEvent = motionEventPredictor.predict(); if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } break; }
Cosa fare e cosa non fare con la previsione dei movimenti
Rimuovi i punti di previsione quando viene aggiunto un nuovo punto previsto.
Non utilizzare punti di previsione per il rendering finale.
App per creare note
ChromeOS consente alla tua app di dichiarare alcune azioni per la creazione di note.
Per registrare un'app come app per prendere appunti su ChromeOS, consulta Compatibilità degli input.
Per registrare un'app come app per creare note su Android, vedi Creare un'app per creare note.
Android 14 (livello API 34) ha introdotto l'intent ACTION_CREATE_NOTE
, che consente alla tua app di avviare un'attività di creazione di note nella schermata di blocco.
Riconoscimento inchiostro digitale con ML Kit
Grazie al riconoscimento inchiostro digitale ML Kit, 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 dai modelli di machine learning per convertire la scrittura a mano libera in testo.
Oltre al riconoscimento della scrittura, il modello è in grado di riconoscere i gesti, come l'eliminazione e il cerchio.
Per scoprire di più, consulta Riconoscimento dell'inchiostro digitale.