1. Avant de commencer
Un stylet est un outil en forme de crayon qui permet aux utilisateurs d'effectuer des tâches précises. Dans cet atelier de programmation, vous allez apprendre à implémenter des expériences de stylet naturelles avec les bibliothèques android.os et androidx. Nous verrons également comment utiliser la classe MotionEvent pour gérer la pression, l'inclinaison et l'orientation, ainsi que la façon d'éviter l'activation tactile par la paume de la main afin d'éviter les gestes involontaires. Enfin, vous apprendrez à réduire la latence du stylet grâce à la prévision de mouvement et aux graphiques à faible latence avec OpenGL et la classe SurfaceView.
Prérequis
- Vous connaissez le langage Kotlin et les lambdas.
- Vous disposez de connaissances de base concernant l'utilisation d'Android Studio.
- Vous disposez de connaissances de base concernant Jetpack Compose.
- Vous disposez de connaissances de base concernant OpenGL pour les graphiques à faible latence.
Points abordés
- Comment utiliser la classe
MotionEventpour le stylet. - Comment implémenter les fonctionnalités du stylet, en particulier la prise en charge de la pression, de l'inclinaison et de l'orientation.
- Comment dessiner sur la classe
Canvas. - Comment implémenter une prévision de mouvement.
- Comment afficher des graphiques à faible latence avec OpenGL et la classe
SurfaceView.
Ce dont vous avez besoin
- La dernière version d'Android Studio.
- Connaissances de la syntaxe du langage Kotlin, y compris les lambdas.
- Expérience de base avec Compose. Si vous ne connaissez pas Compose, suivez l'atelier de programmation Principes de base de Jetpack Compose.
- Un appareil compatible avec les stylets.
- Un stylet fonctionnel.
- Git.
2. Télécharger le code de démarrage
Pour obtenir le code contenant la thématisation et la configuration de base de l'application de démarrage, procédez comme suit :
- Clonez le dépôt GitHub suivant :
git clone https://github.com/android/large-screen-codelabs
- Ouvrez le dossier
advanced-stylus. Le dossierstartcontient le code de démarrage etendle code de solution.
3. Implémenter une application de dessin de base
Tout d'abord, créez la mise en page nécessaire à une application de dessin de base qui permet aux utilisateurs de dessiner tout en affichant les attributs de stylet à l'écran avec la fonction Composable Canvas. Elle se présente comme suit :

La partie supérieure est une fonction Composable Canvas dans laquelle vous dessinez la visualisation du stylet et affichez ses différents attributs, tels que l'orientation, l'inclinaison et la pression. La partie inférieure correspond à une autre fonction Canvas Composable qui reçoit les informations provenant du stylet et les convertit en traits simples.
Pour implémenter la mise en page de base de l'application de dessin, procédez comme suit :
- Dans Android Studio, ouvrez le dépôt cloné.
- Cliquez sur
app>java>com.example.stylus, puis double-cliquez surMainActivity. Le fichierMainActivity.kts'ouvre. - Dans la classe
MainActivity, vous pouvez trouver les fonctionsComposableStylusVisualizationetDrawArea. Dans cette section, vous allez vous concentrer sur la fonctionComposableDrawArea.
Créer une classe StylusState
- Dans le même répertoire
ui, cliquez sur File > New > Kotlin/Class file (Fichier > Nouveau > Fichier/Classe Kotlin). - Dans la zone de texte, remplacez l'espace réservé Name (Nom) par
StylusState.kt, puis appuyez surEnter(oureturnsous macOS). - Dans le fichier
StylusState.kt, créez la classe de donnéesStylusState, puis ajoutez les variables du tableau suivant :
Variable | Type | Valeur par défaut | Description |
|
| Valeur comprise entre 0 et 1.0. | |
|
| Valeur en degrés radians allant de -pi à pi. | |
|
| Valeur en degrés radians comprise entre 0 et pi/2. | |
|
| Stocke les lignes générées par la fonction |
StylusState.kt
package com.example.stylus.ui
import androidx.compose.ui.graphics.Path
data class StylusState(
var pressure: Float = 0F,
var orientation: Float = 0F,
var tilt: Float = 0F,
var path: Path = Path(),
)

- Dans le fichier
MainActivity.kt, recherchez la classeMainActivity, puis ajoutez l'état du stylet avec la fonctionmutableStateOf():
MainActivity.kt
import androidx.compose.runtime.setValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import com.example.stylus.ui.StylusState
class MainActivity : ComponentActivity() {
private var stylusState: StylusState by mutableStateOf(StylusState())
La classe DrawPoint
La classe DrawPoint stocke les données de chaque point dessiné à l'écran. Lorsque vous associez ces points, des lignes sont créées. Ce fonctionnement est semblable à celui de l'objet Path.
La classe DrawPoint étend la classe PointF. Elle contient les données suivantes :
Paramètres | Type | Description |
|
| Coordonnée |
|
| Coordonnée |
|
| Type de point |
Il existe deux types d'objets DrawPoint, décrits par l'énumération DrawPointType :
Type | Description |
| Déplace le début d'une ligne vers une position. |
| Trace une ligne à partir du point précédent. |
DrawPoint.kt
import android.graphics.PointF
class DrawPoint(x: Float, y: Float, val type: DrawPointType): PointF(x, y)
Effectuer le rendu des points de données dans un tracé
Pour cette application, la classe StylusViewModel stocke les données de la ligne, prépare les données pour le rendu et effectue certaines opérations sur l'objet Path pour la désactivation de l'interaction avec la paume de la main.
- Pour conserver les données des lignes, dans la classe
StylusViewModel, créez une liste modifiable d'objetsDrawPoint:
StylusViewModel.kt
import androidx.lifecycle.ViewModel
import com.example.stylus.data.DrawPoint
class StylusViewModel : ViewModel() {private var currentPath = mutableListOf<DrawPoint>()
Pour effectuer le rendu des points de données dans un tracé, procédez comme suit :
- Dans la classe
StylusViewModeldu fichierStylusViewModel.kt, ajoutez une fonctioncreatePath. - Créez une variable
pathde typePathavec le constructeurPath(). - Créez une boucle
fordans laquelle vous itérez chaque point de données de la variablecurrentPath. - Si le point de données est de type
START, appelez la méthodemoveTopour commencer une ligne aux coordonnéesxetyspécifiées. - Sinon, appelez la méthode
lineToavec les coordonnéesxetydu point de données à associer au point précédent. - Renvoyez l'objet
path.
StylusViewModel.kt
import androidx.compose.ui.graphics.Path
import com.example.stylus.data.DrawPoint
import com.example.stylus.data.DrawPointType
class StylusViewModel : ViewModel() {
private var currentPath = mutableListOf<DrawPoint>()
private fun createPath(): Path {
val path = Path()
for (point in currentPath) {
if (point.type == DrawPointType.START) {
path.moveTo(point.x, point.y)
} else {
path.lineTo(point.x, point.y)
}
}
return path
}
private fun cancelLastStroke() {
}
Traiter les objets MotionEvent
Les événements de stylet proviennent d'objets MotionEvent, qui fournissent des informations sur l'action effectuée et les données associées, telles que la position du pointeur et la pression appliquée. Le tableau suivant contient certaines des constantes de l'objet MotionEvent et les données correspondantes. Vous pouvez les utiliser pour identifier les actions de l'utilisateur sur l'écran :
Constante | Données |
| Le pointeur touche l'écran. Il s'agit du début d'une ligne à la position indiquée par l'objet |
| Le pointeur se déplace à l'écran. Une ligne est tracée. |
| Le pointeur cesse de toucher l'écran. C'est la fin de la ligne. |
| Interaction tactile involontaire détectée. Annule le dernier trait. |
Lorsque l'application reçoit un nouvel objet MotionEvent, l'écran doit effectuer le rendu pour refléter les nouvelles entrées utilisateur.
- Pour traiter des objets
MotionEventdans la classeStylusViewModel, créez une fonction qui collecte les coordonnées des lignes :
StylusViewModel.kt
import android.view.MotionEvent
class StylusViewModel : ViewModel() {
private var currentPath = mutableListOf<DrawPoint>()
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
when (motionEvent.actionMasked) {
MotionEvent.ACTION_DOWN -> {
currentPath.add(
DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.START)
)
}
MotionEvent.ACTION_MOVE -> {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
MotionEvent.ACTION_UP -> {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
MotionEvent.ACTION_CANCEL -> {
// Unwanted touch detected.
cancelLastStroke()
}
else -> return false
}
return true
}
Envoyer des données à l'interface utilisateur
Pour mettre à jour la classe StylusViewModel afin que l'interface utilisateur soit informée des modifications apportées à la classe de données StylusState, procédez comme suit :
- Dans la classe
StylusViewModel, créez une variable_stylusStatede typeMutableStateFlowpour la classeStylusStateet une variablestylusStatede typeStateFlowpour la même classe. La variable_stylusStateest modifiée chaque fois que l'état du stylet est modifié dans la classeStylusViewModelet que la variablestylusStateest utilisée par l'UI dans la classeMainActivity.
StylusViewModel.kt
import com.example.stylus.ui.StylusState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
- Créez une fonction
requestRenderingqui reçoit un paramètre d'objetStylusState:
StylusViewModel.kt
import kotlinx.coroutines.flow.update
...
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
...
private fun requestRendering(stylusState: StylusState) {
// Updates the stylusState, which triggers a flow.
_stylusState.update {
return@update stylusState
}
}
- À la fin de la fonction
processMotionEvent, ajoutez un appel de fonctionrequestRenderingavec un paramètreStylusState. - Dans le paramètre
StylusState, récupérez les valeurs d'inclinaison, de pression et d'orientation à partir de la variablemotionEvent, puis créez le tracé avec une fonctioncreatePath(). Cette action déclenche un événement de flux, que vous connecterez ultérieurement à l'UI.
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
else -> return false
}
requestRendering(
StylusState(
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,
path = createPath()
)
)
Lier l'interface utilisateur à la classe StylusViewModel
- Dans la classe
MainActivity, recherchez la fonctionsuper.onCreatede la fonctiononCreate, puis ajoutez la collecte d'état. Pour en savoir plus sur la collecte d'état, consultez la vidéo Collecting flows in a lifecycle-aware manner (Collecter des flux en tenant compte du cycle de vie).
MainActivity.kt
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.onEach
import androidx.lifecycle.Lifecycle
import kotlinx.coroutines.flow.collect
...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.stylusState
.onEach {
stylusState = it
}
.collect()
}
}
Désormais, chaque fois que la classe StylusViewModel publie un nouvel état StylusState, l'activité le reçoit et le nouvel objet StylusState met à jour la variable locale stylusState de la classe MainActivity.
- Dans le corps de la fonction
ComposableDrawArea, ajoutez le modificateurpointerInteropFilterà la fonctionComposableCanvaspour fournir des objetsMotionEvent.
- Envoyez l'objet
MotionEventà la fonctionprocessMotionEventdu StylusViewModel pour traitement :
MainActivity.kt
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.input.pointer.pointerInteropFilter
...
class MainActivity : ComponentActivity() {
...
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
Canvas(modifier = modifier
.clipToBounds()
.pointerInteropFilter {
viewModel.processMotionEvent(it)
}
) {
}
}
- Appelez la fonction
drawPathavec l'attributpathdestylusState, puis indiquez un style de trait et une couleur.
MainActivity.kt
class MainActivity : ComponentActivity() {
...
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
Canvas(modifier = modifier
.clipToBounds()
.pointerInteropFilter {
viewModel.processMotionEvent(it)
}
) {
with(stylusState) {
drawPath(
path = this.path,
color = Color.Gray,
style = strokeStyle
)
}
}
}
- Exécutez l'application. Vous pouvez maintenant dessiner à l'écran.
4. Implémenter la prise en charge de la pression, de l'orientation et de l'inclinaison
Dans la section précédente, vous avez vu comment récupérer des informations provenant du stylet, comme la pression, l'orientation et l'inclinaison, à partir d'objets MotionEvent.
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,
Toutefois, ce raccourci ne fonctionne que pour le premier pointeur. Lorsqu'une interaction multipoint est détectée, plusieurs pointeurs sont activés. Or, ce raccourci ne renvoie que la valeur du premier pointeur à l'écran. Pour demander des données sur un pointeur spécifique, vous pouvez utiliser le paramètre pointerIndex :
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex),
pressure = motionEvent.getPressure(pointerIndex),
orientation = motionEvent.getOrientation(pointerIndex)
Pour en savoir plus sur les pointeurs et l'interaction multipoint, consultez Gérer les gestes tactiles multipoints.
Ajouter une visualisation de la pression, de l'orientation et de l'inclinaison
- Dans le fichier
MainActivity.kt, recherchez la fonctionComposableStylusVisualization, puis utilisez ces informations pour permettre à l'objet de fluxStylusStated'afficher la visualisation :
MainActivity.kt
import StylusVisualization.drawOrientation
import StylusVisualization.drawPressure
import StylusVisualization.drawTilt
...
class MainActivity : ComponentActivity() {
...
@Composable
fun StylusVisualization(modifier: Modifier = Modifier) {
Canvas(
modifier = modifier
) {
with(stylusState) {
drawOrientation(this.orientation)
drawTilt(this.tilt)
drawPressure(this.pressure)
}
}
}
- Exécutez l'application. Trois indicateurs situés en haut de l'écran indiquent l'orientation, la pression et l'inclinaison.
- Dessinez à l'écran avec le stylet, puis observez la manière dont chaque visualisation réagit à vos entrées.

- Inspectez le fichier
StylusVisualization.ktpour comprendre comment chaque visualisation est construite.
5. Désactiver l'interaction avec la paume de la main
L'écran peut enregistrer des interactions tactiles involontaires. C'est par exemple le cas lorsqu'un utilisateur pose naturellement sa main sur l'écran en écrivant du texte.
La désactivation de l'interaction avec la paume de la main est un mécanisme qui détecte ce comportement et avertit le développeur qu'il doit annuler le dernier ensemble d'objets MotionEvent. Un ensemble d'objets MotionEvent commence par la constante ACTION_DOWN.
Vous devez donc conserver un historique des entrées afin de pouvoir supprimer les interactions involontaires de l'écran et afficher à nouveau les entrées utilisateur volontaires. Heureusement, l'historique est déjà stocké dans la classe StylusViewModel, dans la variable currentPath.
Android fournit la constante ACTION_CANCEL de l'objet MotionEvent pour informer le développeur des gestes involontaires. Depuis Android 13, l'objet MotionEvent fournit la constante FLAG_CANCELED qui doit être vérifiée sur la constante ACTION_POINTER_UP.
Implémenter la fonction cancelLastStroke
- Pour supprimer un point de données du dernier point
START, revenez à la classeStylusViewModel, puis créez une fonctioncancelLastStrokequi recherche l'index du dernier point de donnéesSTARTet ne conserve que les données du premier point jusqu'à l'index moins un :
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
private fun cancelLastStroke() {
// Find the last START event.
val lastIndex = currentPath.findLastIndex {
it.type == DrawPointType.START
}
// If found, keep the element from 0 until the very last event before the last MOVE event.
if (lastIndex > 0) {
currentPath = currentPath.subList(0, lastIndex - 1)
}
}
Ajouter les constantes ACTION_CANCEL et FLAG_CANCELED
- Dans le fichier
StylusViewModel.kt, recherchez la fonctionprocessMotionEvent. - Dans la constante
ACTION_UP, créez une variablecanceledqui vérifie si la version actuelle du SDK est Android 13 ou une version ultérieure, et si la constanteFLAG_CANCELEDest activée. - Sur la ligne suivante, créez une structure conditionnelle qui vérifie que la variable
canceledest bien "true". Si tel est le cas, appelez la fonctioncancelLastStrokepour supprimer le dernier ensemble d'objetsMotionEvent. Dans le cas contraire, appelez la méthodecurrentPath.addpour ajouter le dernier ensemble d'objetsMotionEvent.
StylusViewModel.kt
import android.os.Build
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_UP -> {
val canceled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
(motionEvent.flags and MotionEvent.FLAG_CANCELED) == MotionEvent.FLAG_CANCELED
if(canceled) {
cancelLastStroke()
} else {
currentPath.add(DrawPoint(motionEvent.x, motionEvent.y, DrawPointType.LINE))
}
}
- Dans la constante
ACTION_CANCEL, vous pouvez voir la fonctioncancelLastStroke:
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
MotionEvent.ACTION_CANCEL -> {
// unwanted touch detected
cancelLastStroke()
}
La désactivation de l'interaction avec la paume de la main est implémentée ! Le code fonctionnel se trouve dans le dossier palm-rejection.
6. Implémenter une faible latence
Dans cette section, vous allez réduire la latence entre l'entrée utilisateur et le rendu à l'écran afin d'améliorer les performances. La latence peut avoir plusieurs causes, parmi lesquelles un pipeline graphique trop long. Vous pouvez résoudre ce problème grâce au rendu du tampon d'affichage. Cette méthode permet aux développeurs d'accéder directement à la mémoire tampon de l'écran, ce qui permet d'obtenir de bien meilleurs résultats en termes d'écriture manuscrite et de dessin.
La classe GLFrontBufferedRenderer fournie par la bibliothèque androidx.graphics s'occupe du rendu du tampon d'affichage, mais aussi du rendu du tampon doublé. Elle optimise un objet SurfaceView pour permettre un rendu plus rapide avec la fonction de rappel onDrawFrontBufferedLayer et un rendu normal avec la fonction de rappel onDrawDoubleBufferedLayer. La classe GLFrontBufferedRenderer et l'interface GLFrontBufferedRenderer.Callback fonctionnent avec un type de données fourni par l'utilisateur. Dans cet atelier de programmation, vous utiliserez la classe Segment.
Pour l'activer, procédez comme suit :
- Dans Android Studio, ouvrez le dossier
low-latencypour obtenir tous les fichiers requis : - Vous pouvez remarquer que le projet dispose de nouveaux fichiers :
- Dans le fichier
build.gradle, la bibliothèqueandroidx.graphicsa été importée avec la déclarationimplementation "androidx.graphics:graphics-core:1.0.0-alpha03". - La classe
LowLatencySurfaceViewétend la classeSurfaceViewpour afficher le code OpenGL à l'écran. - La classe
LineRenderercontient le code OpenGL permettant d'afficher une ligne à l'écran. - La classe
FastRendererpermet d'accélérer le rendu et implémente l'interfaceGLFrontBufferedRenderer.Callback. Elle intercepte également les objetsMotionEvent. - La classe
StylusViewModelcontient les points de données avec une interfaceLineManager. - La classe
Segmentdéfinit un segment comme suit : x1,y1: coordonnées du premier pointx2,y2: coordonnées du second point
Les images suivantes montrent comment les données se déplacent entre chaque classe :

Créer une surface et une mise en page à faible latence
- Dans le fichier
MainActivity.kt, recherchez la fonctiononCreatede la classeMainActivity. - Dans le corps de la fonction
onCreate, créez un objetFastRenderer, puis transmettez un objetviewModel:
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fastRendering = FastRenderer(viewModel)
lifecycleScope.launch {
...
- Dans le même fichier, créez une fonction
ComposableDrawAreaLowLatency. - Dans le corps de la fonction, utilisez l'API
AndroidViewpour encapsuler la vueLowLatencySurfaceView, puis fournissez l'objetfastRendering:
MainActivity.kt
import androidx.compose.ui.viewinterop.AndroidView
import com.example.stylus.gl.LowLatencySurfaceView
class MainActivity : ComponentActivity() {
...
@Composable
fun DrawAreaLowLatency(modifier: Modifier = Modifier) {
AndroidView(factory = { context ->
LowLatencySurfaceView(context, fastRenderer = fastRendering)
}, modifier = modifier)
}
- Dans la fonction
onCreate, après la fonctionDividerComposable, ajoutez la fonctionComposableDrawAreaLowLatencyà la mise en page :
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
...
Surface(
modifier = Modifier
.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column {
StylusVisualization(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
)
Divider(
thickness = 1.dp,
color = Color.Black,
)
DrawAreaLowLatency()
}
}
- Dans le répertoire
gl, ouvrez le fichierLowLatencySurfaceView.kt. Vous pouvez faire les observations suivantes au sujet de la classeLowLatencySurfaceView:
- La classe
LowLatencySurfaceViewétend la classeSurfaceView. Elle utilise la méthodeonTouchListenerde l'objetfastRenderer. - L'interface
GLFrontBufferedRenderer.Callbackvia la classefastRendererdoit être associée à l'objetSurfaceViewlorsque la fonctiononAttachedToWindowest appelée, afin que les rappels puissent être affichés dans la vueSurfaceView. - L'interface
GLFrontBufferedRenderer.Callbackvia la classefastRendererdoit être libérée lorsque la fonctiononDetachedFromWindowest appelée.
LowLatencySurfaceView.kt
class LowLatencySurfaceView(context: Context, private val fastRenderer: FastRenderer) :
SurfaceView(context) {
init {
setOnTouchListener(fastRenderer.onTouchListener)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
fastRenderer.attachSurfaceView(this)
}
override fun onDetachedFromWindow() {
fastRenderer.release()
super.onDetachedFromWindow()
}
}
Gérer les objets MotionEvent avec l'interface onTouchListener.
Pour gérer les objets MotionEvent lorsque la constante ACTION_DOWN est détectée, procédez comme suit :
- Dans le répertoire
gl, ouvrez le fichierFastRenderer.kt. - Dans le corps de la constante
ACTION_DOWN, créez une variablecurrentXqui stocke la coordonnéexde l'objetMotionEventet une variablecurrentYqui stocke sa coordonnéey. - Créez une variable
Segmentqui stocke un objetSegment. L'objet reçoit deux instances du paramètrecurrentXet deux autres du paramètrecurrentY, car il s'agit du début de la ligne. - Appelez la méthode
renderFrontBufferedLayeravec un paramètresegmentpour déclencher un rappel sur la fonctiononDrawFrontBufferedLayer.
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_DOWN -> {
// Ask that the input system not batch MotionEvent objects,
// but instead deliver them as soon as they're available.
view.requestUnbufferedDispatch(event)
currentX = event.x
currentY = event.y
// Create a single point.
val segment = Segment(currentX, currentY, currentX, currentY)
frontBufferRenderer?.renderFrontBufferedLayer(segment)
}
Pour gérer les objets MotionEvent lorsque la constante ACTION_MOVE est détectée, procédez comme suit :
- Dans le corps de la constante
ACTION_MOVE, créez une variablepreviousXqui stocke la variablecurrentXet une variablepreviousYqui stocke la variablecurrentY. - Créez une variable
currentXqui enregistre la coordonnéexactuelle de l'objetMotionEventet une variablecurrentYqui enregistre sa coordonnéey. - Créez une variable
Segmentqui stocke un objetSegmentrecevant les paramètrespreviousX,previousY,currentXetcurrentY. - Appelez la méthode
renderFrontBufferedLayeravec un paramètresegmentpour déclencher un rappel sur la fonctiononDrawFrontBufferedLayeret exécuter le code OpenGL.
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_MOVE -> {
previousX = currentX
previousY = currentY
currentX = event.x
currentY = event.y
val segment = Segment(previousX, previousY, currentX, currentY)
// Send the short line to front buffered layer: fast rendering
frontBufferRenderer?.renderFrontBufferedLayer(segment)
}
- Pour gérer des objets
MotionEventlorsque la constanteACTION_UPest détectée, appelez la méthodecommitpour déclencher un appel sur la fonctiononDrawDoubleBufferedLayeret exécuter le code OpenGL :
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_UP -> {
frontBufferRenderer?.commit()
}
Implémenter les fonctions de rappel GLFrontBufferedRenderer
Dans le fichier FastRenderer.kt, les fonctions de rappel onDrawFrontBufferedLayer et onDrawDoubleBufferedLayer exécutent le code OpenGL. Au début de chaque fonction de rappel, les fonctions OpenGL suivantes mappent les données Android à l'espace de travail OpenGL :
- La fonction
GLES20.glViewportdéfinit la taille du rectangle dans lequel vous affichez la scène. - La fonction
Matrix.orthoMcalcule la matriceModelViewProjection. - La fonction
Matrix.multiplyMMeffectue une multiplication matricielle pour transformer les données Android en références OpenGL et fournit la configuration pour la matriceprojection.
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDraw[Front/Double]BufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
val bufferWidth = bufferInfo.width
val bufferHeight = bufferInfo.height
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
// Map Android coordinates to OpenGL coordinates.
Matrix.orthoM(
mvpMatrix,
0,
0f,
bufferWidth.toFloat(),
0f,
bufferHeight.toFloat(),
-1f,
1f
)
Matrix.multiplyMM(projection, 0, mvpMatrix, 0, transform, 0)
Une fois cette partie du code configurée, vous pouvez vous concentrer sur le code qui effectue le rendu réel. La fonction de rappel onDrawFrontBufferedLayer affiche une petite zone de l'écran. Elle fournit une valeur param de type Segment afin que vous puissiez afficher rapidement un seul segment. La classe LineRenderer est un moteur de rendu OpenGL pour le pinceau qui applique la couleur et l'épaisseur de la ligne.
Pour implémenter la fonction de rappel onDrawFrontBufferedLayer, procédez comme suit :
- Dans le fichier
FastRenderer.kt, recherchez la fonction de rappelonDrawFrontBufferedLayer. - Dans le corps de la fonction de rappel
onDrawFrontBufferedLayer, appelez la fonctionobtainRendererpour obtenir l'instanceLineRenderer. - Appelez la méthode
drawLinede la fonctionLineRendereravec les paramètres suivants :
- La matrice
projectionprécédemment calculée - Une liste d'objets
Segment(un seul segment dans ce cas) - La valeur
colorde la ligne
FastRenderer.kt
import android.graphics.Color
import androidx.core.graphics.toColor
class FastRenderer( ... ) {
...
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
Matrix.multiplyMM(projection, 0, mvpMatrix, 0, transform, 0)
obtainRenderer().drawLine(projection, listOf(param), Color.GRAY.toColor())
}
- Exécutez l'application. Vous pouvez maintenant dessiner à l'écran avec une latence fortement réduite. Cependant, l'application ne conservera pas la ligne, car vous devez toujours implémenter la fonction de rappel
onDrawDoubleBufferedLayer.
La fonction de rappel onDrawDoubleBufferedLayer est appelée après la fonction commit pour permettre la persistance de la ligne. Le rappel fournit des valeurs params, qui contiennent une collection d'objets Segment. Tous les segments du tampon d'affichage sont relancés dans le double tampon pour permettre leur persistance.
Pour implémenter la fonction de rappel onDrawDoubleBufferedLayer, procédez comme suit :
- Dans le fichier
StylusViewModel.kt, recherchez la classeStylusViewModel, puis créez une variableopenGlLinesqui stocke une liste modifiable d'objetsSegment:
StylusViewModel.kt
import com.example.stylus.data.Segment
class StylusViewModel : ViewModel() {
private var _stylusState = MutableStateFlow(StylusState())
val stylusState: StateFlow<StylusState> = _stylusState
val openGlLines = mutableListOf<List<Segment>>()
private fun requestRendering(stylusState: StylusState) {
- Dans le fichier
FastRenderer.kt, recherchez la fonction de rappelonDrawDoubleBufferedLayerde la classeFastRenderer. - Dans le corps de la fonction de rappel
onDrawDoubleBufferedLayer, effacez le contenu de l'écran à l'aide des méthodesGLES20.glClearColoretGLES20.glClearpour que la scène puisse être entièrement recréée, puis ajoutez les lignes à l'objetviewModelpour qu'elles soient conservées :
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
// Clear the screen with black.
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
viewModel.openGlLines.add(params.toList())
- Créez une boucle
forqui affiche chaque ligne par itération à partir de l'objetviewModel:
FastRenderer.kt
class FastRenderer( ... ) {
...
override fun onDrawDoubleBufferedLayer(
eglManager: EGLManager,
bufferInfo: BufferInfo,
transform: FloatArray,
params: Collection<Segment>
) {
...
// Clear the screen with black.
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
viewModel.openGlLines.add(params.toList())
// Render the entire scene (all lines).
for (line in viewModel.openGlLines) {
obtainRenderer().drawLine(projection, line, Color.GRAY.toColor())
}
}
- Exécutez l'application. Vous pouvez maintenant dessiner à l'écran et la ligne est préservée après le déclenchement de la constante
ACTION_UP.
7. Implémenter la prévision de mouvement
Vous pouvez encore réduire la latence avec la bibliothèque androidx.input, qui analyse le parcours du stylet et prévoit l'emplacement du point suivant pour lancer le rendu.
Pour configurer la prévision de mouvement :
- Dans le fichier
app/build.gradle, importez la bibliothèque dans la section des dépendances :
app/build.gradle
...
dependencies {
...
implementation"androidx.input:input-motionprediction:1.0.0-beta01"
- Cliquez sur File > Sync Project with Gradle Files (Fichier > Synchroniser le projet avec les fichiers Gradle).
- Dans la classe
FastRenderingdu fichierFastRendering.kt, déclarez l'objetmotionEventPredictoren tant qu'attribut :
FastRenderer.kt
import androidx.input.motionprediction.MotionEventPredictor
class FastRenderer( ... ) {
...
private var frontBufferRenderer: GLFrontBufferedRenderer<Segment>? = null
private var motionEventPredictor: MotionEventPredictor? = null
- Dans la fonction
attachSurfaceView, initialisez la variablemotionEventPredictor:
FastRenderer.kt
class FastRenderer( ... ) {
...
fun attachSurfaceView(surfaceView: SurfaceView) {
frontBufferRenderer = GLFrontBufferedRenderer(surfaceView, this)
motionEventPredictor = MotionEventPredictor.newInstance(surfaceView)
}
- Dans la variable
onTouchListener, appelez la méthodemotionEventPredictor?.recordpour que l'objetmotionEventPredictorreçoive les données de mouvement :
FastRendering.kt
class FastRenderer( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
motionEventPredictor?.record(event)
...
when (event?.action) {
L'étape suivante consiste à prévoir un objet MotionEvent avec la fonction predict. Nous vous recommandons de lancer la prévision au moment où une constante ACTION_MOVE est reçue et après l'enregistrement de l'objet MotionEvent. En d'autres termes, vous devez lancer la prévision dès qu'un trait commence.
- Prévoyez un objet
MotionEventartificiel avec la méthodepredict. - Créez un objet
Segmentqui utilise les coordonnées x et y actuelles et prévues. - Demandez un rendu rapide du segment prévu à l'aide de la méthode
frontBufferRenderer?.renderFrontBufferedLayer(predictedSegment).
FastRendering.kt
class FastRenderer( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
motionEventPredictor?.record(event)
...
when (event?.action) {
...
MotionEvent.ACTION_MOVE -> {
...
frontBufferRenderer?.renderFrontBufferedLayer(segment)
val motionEventPredicted = motionEventPredictor?.predict()
if(motionEventPredicted != null) {
val predictedSegment = Segment(currentX, currentY,
motionEventPredicted.x, motionEventPredicted.y)
frontBufferRenderer?.renderFrontBufferedLayer(predictedSegment)
}
}
...
}
Les événements prévus sont insérés dans le moteur de rendu, ce qui réduit la latence.
- Exécutez l'application et faites quelques tests. Vous pouvez observer que la latence est réduite.
Réduire la latence offrira aux utilisateurs de stylet une expérience plus naturelle.
8. Félicitations
Félicitations ! Vous savez gérer le stylet comme un pro.
Vous avez appris à traiter des objets MotionEvent pour extraire des informations sur la pression, l'orientation et l'inclinaison. Vous avez également appris à améliorer la latence en implémentant les bibliothèques androidx.graphics et androidx.input. Ensemble, ces améliorations permettent d'offrir une expérience plus naturelle lors de l'utilisation d'un stylet.