Le stylet permet aux utilisateurs d'interagir de manière confortable et précise avec les applications pour prendre des notes, dessiner, travailler avec des applications de productivité, se détendre et s'amuser avec des applications de jeux et de divertissement.
Android et ChromeOS proposent différentes API pour créer une expérience de stylet exceptionnelle dans les applications. La classe MotionEvent
fournit des informations sur les interactions de l'utilisateur avec l'écran, y compris la pression du stylet, l'orientation, l'inclinaison, le pointage et la détection de la paume de la main. Les bibliothèques de graphiques à faible latence et les bibliothèques de prédiction de mouvement améliorent le rendu à l'écran pour offrir une expérience naturelle proche du dessin traditionnel.
MotionEvent
La classe MotionEvent
représente les interactions d'entrée utilisateur telles que la position et le mouvement des pointeurs tactiles à l'écran. Avec la saisie au stylet, MotionEvent
expose également les données de pression, d'orientation, d'inclinaison et de survol.
Données d'événement
Pour accéder aux données MotionEvent
dans les applications basées sur les vues, configurez un onTouchListener :
Kotlin
val onTouchListener = View.OnTouchListener { view, event -> // Process motion event. }
Java
View.OnTouchListener listener = (view, event) -> { // Process motion event. };
L'écouteur reçoit des objets MotionEvent
du système afin que votre application puisse les traiter.
Un objet MotionEvent
fournit des données sur les aspects suivants d'un événement d'interface utilisateur :
- Actions : interaction physique avec l'appareil (toucher l'écran, déplacer un pointeur sur la surface de l'écran, passer un pointeur au-dessus de la surface de l'écran)
- Pointeurs : identifiants des objets qui interagissent avec l'écran (doigt, stylet, souris)
- Axe : type de données telles que les coordonnées X et Y, la pression, l'inclinaison, l'orientation et le survol (distance)
Actions
Pour implémenter la prise en charge du stylet, vous devez comprendre l'action effectuée par l'utilisateur.
MotionEvent
fournit une grande variété de constantes ACTION
qui définissent des événements de mouvement. Voici les actions les plus importantes pour le stylet :
Action | Description |
---|---|
ACTION_DOWN ACTION_POINTER_DOWN |
Le pointeur a établi un contact avec l'écran. |
ACTION_MOVE | Le pointeur se déplace à l'écran. |
ACTION_UP ACTION_POINTER_UP |
Le pointeur n'est plus en contact avec l'écran. |
ACTION_CANCEL | Intervient lorsque l'ensemble de mouvements précédent ou actuel doit être annulé. |
Votre application peut effectuer des tâches comme commencer un trait avec ACTION_DOWN
, le dessiner avec ACTION_MOVE,
et le terminer lorsqu'ACTION_UP
est déclenché.
L'ensemble d'actions MotionEvent
d'ACTION_DOWN
à ACTION_UP
pour un pointeur donné est appelé "ensemble de mouvements".
Pointeurs
La plupart des écrans sont multipoint : le système attribue un pointeur pour chaque doigt, stylet, souris ou autre objet similaire qui interagit avec l'écran. Un index de pointeur vous permet d'obtenir des informations sur l'axe d'un pointeur spécifique, comme la position du premier doigt touchant l'écran ou du second.
Les index de pointeur sont compris entre zéro et le nombre de pointeurs renvoyés par MotionEvent#pointerCount()
moins 1.
Les valeurs d'axe des pointeurs sont accessibles avec la méthode getAxisValue(axis, pointerIndex)
. Lorsque l'index de pointeur est omis, le système renvoie la valeur du premier pointeur, à savoir zéro (0).
Les objets MotionEvent
contiennent des informations sur le type de pointeur utilisé. Pour obtenir le type de pointeur, itérez les index de pointeur et appelez la méthode getToolType(pointerIndex)
.
Pour en savoir plus sur les pointeurs, consultez Gérer les gestes à plusieurs doigts.
Saisies au stylet
Vous pouvez filtrer les saisies au stylet avec TOOL_TYPE_STYLUS
:
Kotlin
val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)
Java
boolean isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex);
Le stylet peut également signaler qu'il est utilisé comme gomme avec TOOL_TYPE_ERASER
:
Kotlin
val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)
Java
boolean isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex);
Données d'axe concernant le stylet
ACTION_DOWN
et ACTION_MOVE
fournissent des données d'axe concernant le stylet, à savoir les coordonnées X et Y, la pression, l'orientation, l'inclinaison et le survol.
Pour permettre l'accès à ces données, l'API MotionEvent
fournit getAxisValue(int)
, où le paramètre correspond à l'un des identifiants d'axe suivants :
Axe | Valeur renvoyée pour getAxisValue() |
---|---|
AXIS_X |
Coordonnée X d'un événement de mouvement. |
AXIS_Y |
Coordonnée Y d'un événement de mouvement. |
AXIS_PRESSURE |
Sur un écran tactile ou un pavé tactile, il s'agit de la pression appliquée par un doigt, un stylet ou un autre pointeur. Pour une souris ou un trackball, 1 si l'utilisateur appuie sur le bouton principal, 0 dans le cas contraire. |
AXIS_ORIENTATION |
Pour un écran tactile ou un pavé tactile, orientation du doigt, du stylet ou d'un autre pointeur par rapport au plan vertical de l'appareil. |
AXIS_TILT |
Angle d'inclinaison du stylet en radians. |
AXIS_DISTANCE |
Distance du stylet par rapport à l'écran. |
Par exemple, MotionEvent.getAxisValue(AXIS_X)
renvoie la coordonnée X du premier pointeur.
Consultez également Gérer les gestes à plusieurs doigts.
Position
Vous pouvez récupérer les coordonnées X et Y d'un pointeur avec les appels suivants :
MotionEvent#getAxisValue(AXIS_X)
ouMotionEvent#getX()
MotionEvent#getAxisValue(AXIS_Y)
ouMotionEvent#getY()
Pression
Vous pouvez déterminer la pression du pointeur avec les appels suivants :
getAxisValue(AXIS_PRESSURE)
ou getPressure()
pour le premier pointeur
La valeur de pression pour les écrans tactiles ou les pavés tactiles est comprise entre 0 (aucune pression) et 1, mais des valeurs plus élevées peuvent être renvoyées en fonction du calibrage de l'écran.
Orientation
L'orientation indique le sens vers lequel pointe le stylet.
Vous pouvez déterminer l'orientation du pointeur à l'aide de getAxisValue(AXIS_ORIENTATION)
ou de getOrientation()
(pour le premier pointeur).
Pour un stylet, l'orientation est renvoyée sous la forme d'une valeur radian comprise entre 0 et pi (𝛑) dans le sens des aiguilles d'une montre, ou entre 0 et -pi dans le sens inverse des aiguilles d'une montre.
L'orientation vous permet d'implémenter un pinceau réel. Par exemple, si le stylet représente un pinceau plat, la largeur de ce pinceau dépend de l'orientation du stylet.
Inclinaison
Il s'agit ici de l'inclinaison du stylet par rapport à l'écran.
L'inclinaison renvoie l'angle positif du stylet en radians, où zéro signifie "perpendiculaire à l'écran" et 𝛑/2 "à plat sur la surface".
L'angle d'inclinaison peut être déterminé à l'aide de getAxisValue(AXIS_TILT)
(aucun raccourci pour le premier pointeur).
L'inclinaison peut être utilisée pour reproduire aussi fidèlement que possible des outils réels, comme l'imitation d'une ombre avec un crayon incliné.
Survol
La distance entre le stylet et l'écran peut être obtenue avec getAxisValue(AXIS_DISTANCE)
. Cette méthode renvoie une valeur comprise entre 0,0 (contact avec l'écran) et des valeurs plus élevées lorsque le stylet s'éloigne de l'écran. La distance de survol entre l'écran et la pointe du stylet dépend du fabricant de l'écran et du stylet. Étant donné que les implémentations peuvent varier, ne comptez pas sur des valeurs précises pour les fonctionnalités essentielles de votre application.
Vous pouvez utiliser cette option pour prévisualiser la taille du pinceau ou indiquer qu'un bouton sera sélectionné.
Remarque : Compose propose un ensemble d'éléments modificateurs permettant de modifier l'état des éléments d'interface utilisateur.
hoverable
: permet de configurer le composant pour qu'il soit possible de passer le stylet dessus à l'aide d'événements d'entrée et de sortie du pointeur.indication
: permet de dessiner des effets visuels pour ce composant lorsque des interactions se produisent.
Refus de la paume de la main, navigation et entrées indésirables
Parfois, les écrans tactiles multipoints peuvent enregistrer des pressions indésirables, par exemple lorsqu'un utilisateur pose naturellement une partie de la main sur l'écran pendant qu'il écrit. Le refus de la paume de la main est un mécanisme qui détecte ce comportement et vous informe que le dernier MotionEvent
défini doit être annulé.
Vous devez donc conserver un historique des entrées utilisateur pour pouvoir supprimer les éléments indésirables de l'écran et réafficher les entrées légitimes.
ACTION_CANCEL et FLAG_CANCELED
ACTION_CANCEL
et FLAG_CANCELED
sont conçus pour vous informer que l'ensemble MotionEvent
précédent doit être annulé à partir du ACTION_DOWN
précédent pour pouvoir, par exemple, annuler le dernier trait d'une application de dessin pour un pointeur donné.
ACTION_CANCEL
Ajouté dans Android 1.0 (niveau d'API 1).
ACTION_CANCEL
indique que l'ensemble d'événements de mouvement précédent doit être annulé.
ACTION_CANCEL
est déclenché lorsque l'un des éléments suivants est détecté :
- Gestes de navigation
- Refus de la paume de la main
Quand ACTION_CANCEL
est déclenché, vous devez identifier le pointeur actif avec getPointerId(getActionIndex())
. Supprimez ensuite le trait créé avec ce pointeur de l'historique des entrées et réaffichez la scène.
FLAG_CANCELED
Ajouté dans Android 13 (niveau d'API 33).
FLAG_CANCELED
indique que l'éloignement du pointeur était une action involontaire de l'utilisateur. Ce paramètre est généralement défini lorsque l'utilisateur touche accidentellement l'écran, par exemple en saisissant l'appareil dans la main ou en plaçant la paume sur l'écran.
Vous pouvez accéder à la valeur de cet indicateur comme suit :
Kotlin
val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED
Java
boolean cancel = (event.getFlags() & FLAG_CANCELED) == FLAG_CANCELED;
Si l'indicateur est défini, vous devez annuler le dernier MotionEvent
, à partir du dernier événement ACTION_DOWN
de ce pointeur.
Comme ACTION_CANCEL
, le pointeur peut être identifié avec getPointerId(actionIndex)
.
Plein écran, bord à bord et gestes de navigation
Si une application est en plein écran et comporte des éléments exploitables près du bord, comme le canevas d'une application de dessin ou de prise de notes, le balayage de l'écran de bas en haut pour afficher la navigation ou pour mettre l'application en arrière-plan peut engendrer un événement tactile indésirable sur le canevas.
Pour éviter que des gestes ne déclenchent des pressions indésirables dans votre application, vous pouvez utiliser des encarts et ACTION_CANCEL
.
Consultez également la section Refus de la paume de la main, navigation et entrées indésirables ci-dessus.
Utilisez la méthode setSystemBarsBehavior()
et BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
de WindowInsetsController
pour éviter que les gestes de navigation ne provoquent des événements tactiles indésirables :
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 );
Pour en savoir plus sur la gestion des encarts et des gestes, consultez les sections suivantes :
- Masquer les barres système pour le mode immersif
- Assurer la compatibilité avec la navigation par gestes
- Afficher le contenu bord à bord dans votre application
Latence faible
La latence correspond au temps nécessaire au matériel, au système et à l'application pour traiter et afficher l'entrée utilisateur.
Latence = traitement de l'entrée utilisateur par le matériel et l'OS + traitement de l'application + composition du système + rendu matériel
Source de latence
- Enregistrement du stylet avec l'écran tactile (matériel) : connexion initiale sans fil lorsque le stylet et l'OS communiquent pour être enregistrés et synchronisés.
- Taux d'échantillonnage tactile (matériel) : nombre de fois par seconde qu'un écran tactile vérifie si un pointeur touche la surface, valeur compris entre 60 et 1 000 Hz.
- Traitement des entrées (application) : couleurs, effets graphiques et transformations appliquées à l'entrée utilisateur.
- Rendu graphique (OS + matériel) : échange de tampons, traitement matériel.
Graphiques à faible latence
La bibliothèque de graphiques à faible latence Jetpack réduit le temps de traitement entre l'entrée utilisateur et le rendu à l'écran.
Elle réduit le temps de traitement en évitant le rendu multitampon et en exploitant une technique de rendu du tampon d'affichage, qui implique l'écriture directe sur l'écran.
Rendu du tampon d'affichage
Le tampon d'affichage correspond à la mémoire utilisée par l'écran pour le rendu. Il permet aux applications les plus proches de dessiner directement à l'écran. La bibliothèque de graphiques à faible latence permet aux applications d'effectuer le rendu directement dans le tampon d'affichage. Cela permet d'améliorer les performances en empêchant les échanges de tampon, qui peuvent se produire pour le rendu multitampon standard ou le rendu double tampon (cas le plus courant).
Bien que le rendu du tampon d'affichage soit une excellente technique pour une petite zone de l'écran, il n'est pas conçu pour actualiser l'intégralité de l'écran. Avec le rendu du tampon d'affichage, l'application effectue le rendu dans un tampon que l'écran lit directement. Par conséquent, il est possible d'afficher des artefacts ou des données désynchronisées (voir ci-dessous).
La bibliothèque à faible latence est disponible à partir d'Android 10 (niveau d'API 29) et sur les appareils ChromeOS exécutant Android 10 (niveau d'API 29) ou version ultérieure.
Dépendances
La bibliothèque à faible latence fournit les composants nécessaires à l'implémentation du rendu du tampon d'affichage. Elle est ajoutée en tant que dépendance dans le fichier build.gradle
du module de l'application :
dependencies {
implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}
Rappels GLFrontBufferRenderer
La bibliothèque à faible latence inclut l'interface GLFrontBufferRenderer.Callback
, qui définit les méthodes suivantes :
Elle n'est pas spécifique quant au type de données que vous utilisez avec GLFrontBufferRenderer
.
Cependant, la bibliothèque traite les données sous la forme d'un flux composé de centaines de points de données. Il est donc important de concevoir vos données pour optimiser l'utilisation et l'allocation de la mémoire.
Rappels
Pour activer les rappels de rendu, implémentez GLFrontBufferedRenderer.Callback
, puis remplacez onDrawFrontBufferedLayer()
et onDrawDoubleBufferedLayer()
. GLFrontBufferedRenderer
utilise les rappels pour afficher vos données de la manière la plus optimisée possible.
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. } };
Déclarer une instance de GLFrontBufferedRenderer
Préparez le GLFrontBufferedRenderer
en fournissant l'élément SurfaceView
et les rappels que vous avez créés précédemment. GLFrontBufferedRenderer
optimise le rendu dans le tampon d'affichage et le double tampon à l'aide de vos rappels :
Kotlin
var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Java
GLFrontBufferedRenderer<DATA_TYPE> glFrontBufferRenderer = new GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks);
Rendu
Le rendu du tampon d'affichage commence lorsque vous appelez la méthode renderFrontBufferedLayer()
, ce qui déclenche le rappel onDrawFrontBufferedLayer()
.
Le rendu en double tampon reprend lorsque vous appelez la fonction commit()
, ce qui déclenche le rappel onDrawMultiDoubleBufferedLayer()
.
Dans l'exemple ci-dessous, le processus effectue le rendu dans le tampon d'affichage (rendu rapide) lorsque l'utilisateur commence à dessiner à l'écran (ACTION_DOWN
) et qu'il déplace le pointeur (ACTION_MOVE
). Il effectue le rendu dans le double tampon lorsque le pointeur quitte la surface de l'écran (ACTION_UP
).
Vous pouvez utiliser requestUnbufferedDispatch()
pour demander au système d'entrée de ne pas regrouper les événements de mouvement, mais de les livrer dès qu'ils sont disponibles :
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; }
Bonnes pratiques et pratiques interdites concernant le rendu
À faire
Petites parties de l'écran, écriture manuscrite, dessin, croquis.
À éviter
Mise à jour en plein écran, panoramique, zoom. Peut entraîner une désynchronisation des données.
Désynchronisation des données
La désynchronisation des données se produit lorsque l'écran est actualisé pendant que la mémoire tampon de l'écran est modifiée. Une partie de l'écran affiche donc les nouvelles données, et une autre les anciennes.
Prédiction de mouvement
La bibliothèque de prédiction de mouvement Jetpack réduit la latence perçue en estimant le tracé du trait de l'utilisateur et en fournissant des points artificiels temporaires au moteur de rendu.
La bibliothèque de prédiction de mouvement reçoit les entrées utilisateur réelles en tant qu'objets MotionEvent
. Les objets contiennent des informations sur les coordonnées X et Y, la pression et le temps. Ils sont alors exploités par le prédicteur de mouvement pour prédire les futurs objets MotionEvent
.
Les objets MotionEvent
prédits ne sont que des estimations. Les événements prédits peuvent réduire la latence perçue, mais les données prédites doivent être remplacées par des données MotionEvent
réelles une fois qu'elles ont été reçues.
La bibliothèque de prédiction de mouvement est disponible sur Android 4.4 (niveau d'API 19) ou version ultérieure, ainsi que sur les appareils ChromeOS exécutant Android 9 (niveau d'API 28) ou version ultérieure.
Dépendances
La bibliothèque de prédiction de mouvement fournit l'implémentation de la prédiction. Elle est ajoutée en tant que dépendance dans le fichier build.gradle
du module de l'application :
dependencies {
implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}
Implémentation
La bibliothèque de prédiction de mouvement inclut l'interface MotionEventPredictor
, qui définit les méthodes suivantes :
record()
: stocke les objetsMotionEvent
pour enregistrer les actions de l'utilisateur.predict()
: renvoie unMotionEvent
prédit.
Déclarer une instance de MotionEventPredictor
Kotlin
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Java
MotionEventPredictor motionEventPredictor = MotionEventPredictor.newInstance(surfaceView);
Transmettre des données au prédicteur
Kotlin
motionEventPredictor.record(motionEvent)
Java
motionEventPredictor.record(motionEvent);
Prédire
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; }
Bonnes pratiques et pratiques interdites concernant les prédictions de mouvement
À faire
Supprimez les points de prédiction lorsqu'un point de prédiction est ajouté.
À éviter
N'utilisez pas de points de prédiction pour le rendu final.
Applications de prise de notes
ChromeOS permet à votre application de déclarer des actions de prise de notes.
Pour enregistrer une application en tant qu'application de prise de notes sous ChromeOS, consultez l'article Compatibilité de la saisie.
Pour enregistrer une application en tant qu'application de prise de notes sur Android, consultez l'article Créer une application de prise de notes.
Android 14 (niveau d'API 34) a introduit l'intent ACTION_CREATE_NOTE
, qui permet à votre application de démarrer une activité de prise de notes sur l'écran de verrouillage.
Reconnaissance d'encre numérique avec ML Kit
Grâce à la reconnaissance d'encre numérique avec ML Kit, votre application peut reconnaître du texte manuscrit sur une surface numérique dans des centaines de langues. Vous pouvez également classer les croquis.
ML Kit fournit la classe Ink.Stroke.Builder
pour créer des objets Ink
pouvant être traités par des modèles de machine learning pour convertir l'écriture manuscrite en texte.
En plus de la reconnaissance de l'écriture manuscrite, ce modèle est capable de reconnaître des gestes, tels que la suppression ou les cercles.
Pour en savoir plus, consultez la section Reconnaissance de l'encre numérique.