Fonctionnalités avancées du stylet

Android et ChromeOS fournissent diverses API pour vous aider à créer des applications qui offrent aux utilisateurs une expérience exceptionnelle avec le stylet. La classe MotionEvent expose des informations sur l'interaction du stylet avec l'écran, y compris la pression du stylet, l'orientation, l'inclinaison, le survol 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 semblable à celle d'un stylo et d'une feuille de papier.

MotionEvent

La classe MotionEvent représente les interactions d'entrée utilisateur telles que la position et le mouvement des pointeurs tactiles à l'écran. Pour 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, ajoutez un modificateur pointerInput aux composants:

@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 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, pointer sur la surface de l'écran)
  • Pointeurs: identifiants des objets qui interagissent avec l'écran (doigt, stylet, souris)
  • Axe: type de données (coordonnées X et Y, pression, inclinaison, orientation et survol (distance))

Actions

Pour implémenter la prise en charge des stylets, 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 lorsque ACTION_DOWN se produit, le dessiner avec ACTION_MOVE, et le terminer lorsque ACTION_UP est déclenché.

L'ensemble d'actions MotionEvent de 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 à chaque doigt, stylet, souris ou autre objet pointant 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 la section Gérer les gestes à plusieurs doigts.

Saisies au stylet

Vous pouvez filtrer les saisies au stylet avec TOOL_TYPE_STYLUS:

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

Le stylet peut également indiquer qu'il est utilisé comme gomme avec TOOL_TYPE_ERASER:

val 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 la section 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 :

Dessin d'un stylet à l'écran avec mappage des coordonnées X et Y
Figure 1 : Coordonnées X et Y d'un stylet

Pression

Vous pouvez connaître la pression du pointeur avec MotionEvent#getAxisValue(AXIS_PRESSURE) ou, pour le premier pointeur, MotionEvent#getPressure().

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 de l'étalonnage de l'écran.

Type de trait représentant une pression continue allant de faible à élevée Le trait est étroit et léger à gauche, ce qui indique une faible pression. Plus il se dirige vers la droite, plus le trait s'élargit et s'intensifie, ce qui indique une pression de plus en plus forte.
Figure 2 : Représentation de la pression : faible pression à gauche et forte pression à droite

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, sa largeur dépend de son orientation.

Figure 3 : Stylet pointant vers la gauche d'environ 0,57 radian

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 est 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 permet de reproduire le plus près possible des outils réels, par exemple en imitant l'ombrage à l'aide d'un crayon incliné.

Stylet incliné d'environ 40 degrés par rapport à la surface de l'écran
Figure 4 : Stylet incliné à environ 0,785 radian, soit 45 degrés par rapport au niveau perpendiculaire

Pointer

La distance entre le stylet et l'écran peut être obtenue avec getAxisValue(AXIS_DISTANCE). La 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 l'application.

Vous pouvez utiliser le stylet pour prévisualiser la taille du pinceau ou indiquer qu'un bouton sera sélectionné.

Figure 5 : Stylet au-dessus d'un écran. L'application réagit même si le stylet ne touche pas la surface de l'écran.

Remarque:Compose fournit des modificateurs qui affectent l'état interactif des éléments de l'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 multipoint peuvent enregistrer des pressions indésirables, par exemple lorsqu'un utilisateur pose naturellement sa main sur l'écran pour obtenir de l'aide pendant l'écriture manuscrite. 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é.

Par conséquent, vous devez conserver un historique des entrées utilisateur afin que les gestes non désirés puissent être supprimés de l'écran et que les entrées utilisateur légitimes puissent être affichées à nouveau.

ACTION_CANCEL et FLAG_CANCELED

ACTION_CANCEL et FLAG_CANCELED sont tous deux conçus pour vous informer que l'ensemble MotionEvent précédent doit être annulé à partir du dernier ACTION_DOWN, afin que vous puissiez, 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

Lorsque 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 le pointeur était une action involontaire de l'utilisateur. L'indicateur est généralement défini lorsque l'utilisateur touche accidentellement l'écran, par exemple en saisissant l'appareil ou en plaçant la paume de la main sur l'écran.

Vous pouvez accéder à la valeur de cet indicateur comme suit :

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

Si l'indicateur est défini, vous devez annuler le dernier MotionEvent, à partir du dernier ACTION_DOWN de ce pointeur.

Comme ACTION_CANCEL, le pointeur peut être identifié avec getPointerId(actionIndex).

Figure 6 : Les traits et le toucher avec la paume de la main créent des ensembles MotionEvent. La pression de la paume est annulée, et l'écran est à nouveau affiché.

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, tels que le canevas d'une application de dessin ou de prise de notes, balayer l'écran à partir du bas de l'écran pour afficher la navigation ou déplacer l'application en arrière-plan peut entraîner une pression indésirable sur le canevas.

Figure 7 : Balayage de l'écran pour mettre une application en arrière-plan

Pour empêcher les gestes de déclencher 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.

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:

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

Pour en savoir plus sur la gestion des encarts et des gestes, consultez les sections suivantes :

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 des entrées par matériel et système d'exploitation + traitement de l'application + composition du système

  • rendu matériel
La latence entraîne un retard du trait affiché après le positionnement du stylet. L'écart entre le trait affiché et la position du stylet représente la latence.
Figure 8. La latence entraîne un retard du trait affiché après le positionnement du stylet.

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, compris entre 60 et 1 000 Hz.
  • Traitement des entrées (application): appliquer des couleurs, des effets graphiques et des transformations à l'entrée utilisateur.
  • Rendu graphique (OS + matériel) : échange de tampons, traitement matériel.

Graphiques à faible latence

La bibliothèque graphique à faible latence Jetpack réduit le temps de traitement entre l'entrée utilisateur et le rendu à l'écran.

Cette bibliothèque réduit le temps de traitement en évitant le rendu multitampon et en exploitant une technique de rendu du tampon d'affichage, qui consiste à écrire directement 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 s'agit des applications les plus proches qui peuvent dessiner directement à l'écran. La bibliothèque à faible latence permet aux applications d'effectuer le rendu directement dans le tampon d'affichage. Cela améliore les performances en empêchant le remplacement de tampon, qui peut se produire pour le rendu multitampon standard ou le rendu double tampon (cas le plus courant).

L'application lit et écrit dans le tampon de l'écran.
Figure 9 : Rendu du tampon d'affichage
L'application écrit dans le multitampon, qui remplace le tampon de l'écran. L'application lit les données à partir du tampon de l'écran.
Figure 10 : Rendu multitampon

Bien que le rendu du tampon d'affichage soit une excellente technique pour afficher 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 du contenu dans un tampon que l'écran lit. Par conséquent, il est possible d'afficher les artefacts ou de créer des données désynchronisées (voir ci-dessous).

La bibliothèque à faible latence est disponible à partir d'Android 10 (niveau d'API 29) ou version ultérieure, ainsi que 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 pour la mise en œuvre du rendu dans le tampon d'affichage. La bibliothèque 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:

La bibliothèque à faible latence n'est pas déterminée en fonction du type de données que vous utilisez avec GLFrontBufferRenderer.

Cependant, la bibliothèque traite les données comme un flux de centaines de points de données. Vous devez donc concevoir vos données de manière à optimiser l'utilisation et l'allocation de la mémoire.

Rappels

Pour activer les rappels de rendu, implémentez GLFrontBufferedRenderer.Callback, et remplacez onDrawFrontBufferedLayer() et onDrawDoubleBufferedLayer(). GLFrontBufferedRenderer utilise les rappels pour afficher vos données de la manière la plus optimisée possible.

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.
   }
}
Déclarer une instance de GLFrontBufferedRenderer

Préparez le GLFrontBufferedRenderer en fournissant le 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:

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

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 qui suit, 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 diffuser dès qu'ils sont 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()
   }
}

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

Un déchirure se produit lorsque l'écran s'actualise alors que le tampon de l'écran est en cours de modification en même temps. Une partie de l'écran affiche les nouvelles données, tandis qu'une autre affiche les anciennes.

Les parties supérieure et inférieure de l&#39;image Android ne concordent pas en raison de la désynchronisation des données pendant l&#39;actualisation de l&#39;écran.
Figure 11 : Désynchronisation des données due à l'actualisation de l'écran de haut en bas

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, qui sont exploitées 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évus 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 à partir d'Android 4.4 (niveau d'API 19) ou version ultérieure, ainsi que sur les appareils ChromeOS équipés d'Android 9 (niveau d'API 28) ou version ultérieure.

La latence entraîne un retard du trait affiché après le positionnement du stylet. L&#39;espace entre le trait et le stylet est rempli par des points de prédiction. L&#39;écart restant est la latence perçue.
Figure 12 : Latence réduite par la prédiction du mouvement.

Dépendances

La bibliothèque de prédiction de mouvement fournit l'implémentation de la prédiction. La bibliothèque 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 objets MotionEvent pour enregistrer les actions de l'utilisateur.
  • predict() : renvoie un MotionEvent prédit.
Déclarer une instance de MotionEventPredictor
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Transmettre des données au prédicteur
motionEventPredictor.record(motionEvent)
Prédire

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

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 la section Compatibilité de la saisie.

Pour enregistrer une application en tant qu'application de prise de notes sur Android, consultez 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 numérique d'encre 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 afin de convertir l'écriture manuscrite en texte.

En plus de la reconnaissance de l'écriture manuscrite, le modèle est capable de reconnaître des gestes, tels que la suppression et le cercle.

Pour en savoir plus, consultez la section Reconnaissance de l'encre numérique.

Ressources supplémentaires

Guides du développeur

Codelabs