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
.
Conditions préalables
- 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
MotionEvent
pour 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.
- Vous connaissez la syntaxe du langage Kotlin, y compris les lambdas.
- Vous disposez d'une expérience de base avec Compose. Dans le cas contraire, 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 dossierstart
contient le code de démarrage etend
le 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.kt
s'ouvre. - Dans la classe
MainActivity
, vous pouvez trouver les fonctionsComposable
StylusVisualization
etDrawArea
. Dans cette section, vous allez vous concentrer sur la fonctionComposable
DrawArea
.
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
(oureturn
sous 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
StylusViewModel
du fichierStylusViewModel.kt
, ajoutez une fonctioncreatePath
. - Créez une variable
path
de typePath
avec le constructeurPath()
. - Créez une boucle
for
dans 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éthodemoveTo
pour commencer une ligne aux coordonnéesx
ety
spécifiées. - Sinon, appelez la méthode
lineTo
avec les coordonnéesx
ety
du 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
MotionEvent
dans 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_stylusState
de typeMutableStateFlow
pour la classeStylusState
et une variablestylusState
de typeStateFlow
pour la même classe. La variable_stylusState
est modifiée chaque fois que l'état du stylet est modifié dans la classeStylusViewModel
et que la variablestylusState
est 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
requestRendering
qui 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 fonctionrequestRendering
avec 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.onCreate
de 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 local stylusState
de la classe MainActivity
.
- Dans le corps de la fonction
Composable
DrawArea
, ajoutez le modificateurpointerInteropFilter
à la fonctionComposable
Canvas
pour fournir des objetsMotionEvent
.
- Envoyez l'objet
MotionEvent
à la fonctionprocessMotionEvent
du 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
drawPath
avec l'attributpath
destylusState
, 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, tels que 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 fonctionComposable
StylusVisualization
, puis utilisez ces informations pour permettre à l'objet de fluxStylusState
d'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.kt
pour 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 fonctioncancelLastStroke
qui recherche l'index du dernier point de donnéesSTART
et 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 variablecanceled
qui vérifie si la version actuelle du SDK est Android 13 ou une version ultérieure, et si la constanteFLAG_CANCELED
est activée. - Sur la ligne suivante, créez une structure conditionnelle qui vérifie que la variable
canceled
est bien "true". Si tel est le cas, appelez la fonctioncancelLastStroke
pour supprimer le dernier ensemble d'objetsMotionEvent
. Dans le cas contraire, appelez la méthodecurrentPath.add
pour 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 doublé du tampon. 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-latency
pour obtenir tous les fichiers requis : - Vous pouvez remarquer que le projet dispose de nouveaux fichiers :
- Dans le fichier
build.gradle
, la bibliothèqueandroidx.graphics
a été importée avec la déclarationimplementation "androidx.graphics:graphics-core:1.0.0-alpha03"
. - La classe
LowLatencySurfaceView
étend la classeSurfaceView
pour afficher le code OpenGL à l'écran. - La classe
LineRenderer
contient le code OpenGL permettant d'afficher une ligne à l'écran. - La classe
FastRenderer
permet d'accélérer le rendu et implémente l'interfaceGLFrontBufferedRenderer.Callback
. Elle intercepte également les objetsMotionEvent
. - La classe
StylusViewModel
contient les points de données avec une interfaceLineManager
. - La classe
Segment
dé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 fonctiononCreate
de 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
Composable
DrawAreaLowLatency
. - Dans le corps de la fonction, utilisez l'API
AndroidView
pour 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 fonctionDivider
Composable
, ajoutez la fonctionComposable
DrawAreaLowLatency
à 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éthodeonTouchListener
de l'objetfastRenderer
. - L'interface
GLFrontBufferedRenderer.Callback
via la classefastRenderer
doit être associée à l'objetSurfaceView
lorsque la fonctiononAttachedToWindow
est appelée, afin que les rappels puissent être affichés dans la vueSurfaceView
. - L'interface
GLFrontBufferedRenderer.Callback
via la classefastRenderer
doit être libérée lorsque la fonctiononDetachedFromWindow
est 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 variablecurrentX
qui stocke la coordonnéex
de l'objetMotionEvent
et une variablecurrentY
qui stocke sa coordonnéey
. - Créez une variable
Segment
qui stocke un objetSegment
. L'objet reçoit deux instances du paramètrecurrentX
et deux autres du paramètrecurrentY
, car il s'agit du début de la ligne. - Appelez la méthode
renderFrontBufferedLayer
avec un paramètresegment
pour 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 variablepreviousX
qui stocke la variablecurrentX
et une variablepreviousY
qui stocke la variablecurrentY
. - Créez une variable
currentX
qui enregistre la coordonnéex
actuelle de l'objetMotionEvent
et une variablecurrentY
qui enregistre sa coordonnéey
. - Créez une variable
Segment
qui stocke un objetSegment
recevant les paramètrespreviousX
,previousY
,currentX
etcurrentY
. - Appelez la méthode
renderFrontBufferedLayer
avec un paramètresegment
pour déclencher un rappel sur la fonctiononDrawFrontBufferedLayer
et 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
MotionEvent
lorsque la constanteACTION_UP
est détectée, appelez la méthodecommit
pour déclencher un appel sur la fonctiononDrawDoubleBufferedLayer
et 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.glViewport
définit la taille du rectangle dans lequel vous affichez la scène. - La fonction
Matrix.orthoM
calcule la matriceModelViewProjection
. - La fonction
Matrix.multiplyMM
effectue 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 fonctionobtainRenderer
pour obtenir l'instanceLineRenderer
. - Appelez la méthode
drawLine
de la fonctionLineRenderer
avec les paramètres suivants :
- La matrice
projection
précédemment calculée - Une liste d'objets
Segment
(un seul segment dans ce cas) - La valeur
color
de 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 variableopenGlLines
qui 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 rappelonDrawDoubleBufferedLayer
de la classeFastRenderer
. - Dans le corps de la fonction de rappel
onDrawDoubleBufferedLayer
, effacez le contenu de l'écran à l'aide des méthodesGLES20.glClearColor
etGLES20.glClear
pour que la scène puisse être entièrement recréée, puis ajoutez les lignes à l'objetviewModel
pour 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
for
qui 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
FastRendering
du fichierFastRendering.kt
, déclarez l'objetmotionEventPredictor
en 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?.record
pour que l'objetmotionEventPredictor
reç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
MotionEvent
artificiel avec la méthodepredict
. - Créez un objet
Segment
qui 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.