1. Antes de comenzar
Una pluma stylus es una herramienta con forma de lápiz que ayuda a los usuarios a realizar tareas precisas. En este codelab, aprenderás a implementar experiencias orgánicas de la pluma stylus con las bibliotecas android.os y androidx. También aprenderás a usar la clase MotionEvent para admitir la presión, la inclinación y la orientación, así como el rechazo de la palma para evitar toques no deseados. Además, aprenderás a reducir la latencia de la pluma stylus con la predicción de movimiento y gráficos de baja latencia con OpenGL y la clase SurfaceView.
Requisitos previos
- Experiencia con Kotlin y lambdas
- Conocimientos básicos de Android Studio
- Conocimientos básicos de Jetpack Compose
- Conocimientos básicos de OpenGL para gráficos de baja latencia
Qué aprenderás
- Cómo usar la clase
MotionEventpara la pluma stylus - Cómo implementar las capacidades de la pluma stylus, incluida la compatibilidad con la presión, la inclinación y la orientación
- Cómo dibujar en la clase
Canvas - Cómo implementar la predicción de movimiento
- Cómo renderizar gráficos de baja latencia con OpenGL y la clase
SurfaceView
Requisitos
- La versión más reciente de Android Studio
- Tener experiencia con la sintaxis de Kotlin, incluidas las funciones lambdas
- Tener experiencia básica con Compose (si no estás familiarizado con Compose, completa el codelab Aspectos básicos de Jetpack Compose)
- Un dispositivo que admita plumas stylus
- Una pluma stylus activa
- Git
2. Obtén el código de partida
Para obtener el código que contiene los temas y la configuración básica de la app de partida, sigue estos pasos:
- Clona este repositorio de GitHub:
git clone https://github.com/android/large-screen-codelabs
- Abre la carpeta
advanced-stylus. La carpetastartincluye el código de partida, y la carpetaend, el código de la solución.
3. Implementa una app básica de dibujo
Primero, compilarás el diseño necesario para una app de dibujo básica que les permite a los usuarios dibujar y muestra atributos de la pluma stylus en la pantalla con la función Composable de Canvas. Se ve como la siguiente imagen:

La parte superior es una función Composable de Canvas en la que dibujas la visualización de la pluma stylus y muestras sus diferentes atributos, como su orientación, inclinación y presión. La parte inferior es otra función Composable de Canvas que recibe la entrada de la pluma stylus y dibuja trazos simples.
Para implementar el diseño básico de la app de dibujo, sigue estos pasos:
- En Android Studio, abre el repositorio clonado.
- Haz clic en
app>java>com.example.stylusy haz doble clic enMainActivity. Se abrirá el archivoMainActivity.kt. - En la clase
MainActivity, observa las funcionesComposabledeStylusVisualizationyDrawArea. En esta sección, te enfocas en la funciónComposabledeDrawArea.
Crea una clase StylusState.
- En el mismo directorio
ui, haz clic en File > New > Kotlin/Class file. - En el cuadro de texto, reemplaza el marcador de posición Name por
StylusState.kty, luego, presionaEnter(oreturnen macOS). - En el archivo
StylusState.kt, crea la clase de datosStylusStatey, luego, agrega las variables de la siguiente tabla:
Variable | Tipo | Valor predeterminado | Descripción |
|
| Es un valor que varía de 0 a 1.0. | |
|
| Es un valor de radianes que varía de -pi a pi. | |
|
| Es un valor de radianes que varía de 0 a pi/2. | |
|
| Almacena líneas renderizadas por la función |
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(),
)

- En el archivo
MainActivity.kt, busca la claseMainActivityy, luego, agrega el estado de la pluma stylus con la funciónmutableStateOf():
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 clase DrawPoint
La clase DrawPoint almacena datos sobre cada punto dibujado en la pantalla; cuando vincules estos puntos, crearás líneas. Imita cómo funciona el objeto Path.
La clase DrawPoint extiende la clase PointF. Contiene los siguientes datos:
Parámetros | Tipo | Descripción |
|
| Coordenada |
|
| Coordenada |
|
| Tipo de punto |
Existen dos tipos de objetos DrawPoint, que describe el enum DrawPointType:
Tipo | Descripción |
| Mueve el inicio de una línea a una posición. |
| Traza una línea desde el punto anterior. |
DrawPoint.kt
import android.graphics.PointF
class DrawPoint(x: Float, y: Float, val type: DrawPointType): PointF(x, y)
Renderiza los datos en una ruta de acceso
En esta app, la clase StylusViewModel contiene datos de la línea, prepara datos para la renderización y realiza algunas operaciones en el objeto Path para el rechazo de la palma.
- Para contener los datos de las líneas, en la clase
StylusViewModel, crea una lista mutable de objetosDrawPoint:
StylusViewModel.kt
import androidx.lifecycle.ViewModel
import com.example.stylus.data.DrawPoint
class StylusViewModel : ViewModel() {private var currentPath = mutableListOf<DrawPoint>()
Para renderizar los datos en una ruta, sigue estos pasos:
- En la clase
StylusViewModeldel archivoStylusViewModel.kt, agrega una funcióncreatePath. - Crea una variable
pathde tipoPathcon el constructorPath(). - Crea un bucle
foren el que iteres a través de cada dato en la variablecurrentPath. - Si los datos son del tipo
START, llama al métodomoveTopara iniciar una línea en las coordenadasxyyespecificadas. - De lo contrario, llama al método
lineTocon las coordenadasxyyde los datos para vincularlos al punto anterior. - Devuelve el objeto
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() {
}
Procesa objetos MotionEvent
Los eventos de la pluma stylus llegan a través de los objetos MotionEvent, que proporcionan información sobre la acción realizada y los datos asociados a esta, como la posición del puntero y la presión. En la siguiente tabla, se incluyen algunas de las constantes del objeto MotionEvent y sus datos, que puedes usar para identificar lo que hace el usuario en la pantalla:
Constante | Datos |
| El puntero toca la pantalla. Es el inicio de una línea en la posición que informa el objeto |
| El puntero se mueve en la pantalla. Es la línea que se dibuja. |
| El puntero deja de tocar la pantalla. Es el final de la línea. |
| Se detectó un toque no deseado. Cancela el último trazo. |
Cuando la app recibe un nuevo objeto MotionEvent, se debe renderizar la pantalla para reflejar la nueva entrada del usuario.
- Para procesar objetos
MotionEventen la claseStylusViewModel, crea una función que reúna las coordenadas de las líneas:
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
}
Envía datos a la IU
Para actualizar la clase StylusViewModel, de modo que la IU pueda recopilar cambios en la clase de datos StylusState, sigue estos pasos:
- En la clase
StylusViewModel, crea una variable_stylusStatede un tipoMutableStateFlowde la claseStylusStatey una variablestylusStatede un tipoStateFlowde la claseStylusState. La variable_stylusStatese modifica cada vez que se cambia el estado de la pluma stylus en la claseStylusViewModely la IU consume la variablestylusStateen la claseMainActivity.
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
- Crea una función
requestRenderingque acepte un parámetro del objetoStylusState:
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
}
}
- Al final de la función
processMotionEvent, agrega una llamada a la funciónrequestRenderingcon un parámetroStylusState. - En el parámetro
StylusState, recupera los valores de inclinación, presión y orientación de la variablemotionEventy, luego, crea la ruta con una funcióncreatePath(). De esta manera, se activa un evento de flujo, que se conectará más adelante en la IU.
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()
)
)
Vincula la IU con la clase StylusViewModel
- En la clase
MainActivity, busca la funciónsuper.onCreatede la funciónonCreatey, luego, agrega la recopilación de estado. Para obtener más información sobre la recopilación de estados, consulta el video Collecting flows in a lifecycle-aware manner (Cómo recopilar flujos de manera optimizada para ciclos de vida).
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()
}
}
Ahora, cada vez que la clase StylusViewModel publica un nuevo estado StylusState, la actividad lo recibe, y el nuevo objeto StylusState actualiza la variable stylusState de la clase MainActivity local.
- En el cuerpo de la función
ComposabledeDrawArea, agrega el modificadorpointerInteropFiltera la funciónComposabledeCanvaspara proporcionar objetosMotionEvent.
- Envía el objeto
MotionEventa la funciónprocessMotionEventde StylusViewModel para su procesamiento:
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)
}
) {
}
}
- Llama a la función
drawPathcon el atributopathdestylusStatey, luego, proporciona un color y un estilo de trazo.
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
)
}
}
}
- Ejecuta la app y, luego, observa que puedes dibujar en la pantalla.
4. Implementa la compatibilidad con la presión, la orientación y la inclinación
En la sección anterior, viste cómo recuperar información de la pluma stylus desde objetos MotionEvent, como la presión, la orientación y la inclinación.
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,
Sin embargo, esta combinación de teclas solo funciona para el primer puntero. Cuando se detectan varios toques, se detectan varios punteros, y este acceso directo solo muestra el valor del primer puntero (o del primer puntero en la pantalla). Para solicitar datos sobre un puntero específico, puedes usar el parámetro pointerIndex:
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex),
pressure = motionEvent.getPressure(pointerIndex),
orientation = motionEvent.getOrientation(pointerIndex)
Para obtener más información sobre los punteros y varios toques, consulta Cómo controlar gestos de varios toques.
Agrega visualización para la presión, la orientación y la inclinación
- En el archivo
MainActivity.kt, busca la funciónComposabledeStylusVisualizationy, luego, usa la información del objeto de flujoStylusStatepara renderizar la visualización:
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)
}
}
}
- Ejecuta la app. Verás tres indicadores en la parte superior de la pantalla que indican la orientación, la inclinación y la presión.
- Con una pluma stylus, haz garabatos en la pantalla y observa cómo reacciona cada visualización con las entradas.

- Inspecciona el archivo
StylusVisualization.ktpara comprender cómo se construye cada visualización.
5. Implementa el rechazo de la palma
La pantalla puede registrar toques no deseados. Por ejemplo, ocurre cuando un usuario apoya naturalmente la mano en la pantalla para descansar mientras escribe.
El rechazo de la palma es un mecanismo que detecta este comportamiento y notifica al desarrollador para que cancele el último conjunto de objetos MotionEvent. Un conjunto de objetos MotionEvent comienza con la constante ACTION_DOWN.
Es decir, debes mantener un historial de las entradas para que puedas quitar los toques no deseados de la pantalla y volver a renderizar las verdaderas entradas de los usuarios. Afortunadamente, ya tienes el historial almacenado en la clase StylusViewModel de la variable currentPath.
Android proporciona la constante ACTION_CANCEL del objeto MotionEvent para informarle al desarrollador el toque no deseado. A partir de Android 13, el objeto MotionEvent proporciona la constante FLAG_CANCELED que debe verificarse en la constante ACTION_POINTER_UP.
Implementa la función cancelLastStroke
- Para quitar un dato del último dato
START, regresa a la claseStylusViewModely, luego, crea una funcióncancelLastStrokeque encuentre el índice del último datoSTARTy que solo conserve los datos desde el primer dato hasta el índice menos uno:
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)
}
}
Agrega las constantes ACTION_CANCEL y FLAG_CANCELED.
- En el archivo
StylusViewModel.kt, busca la funciónprocessMotionEvent. - En la constante
ACTION_UP, crea una variablecanceledque verifica si la versión actual del SDK es Android 13 o una posterior, y si la constanteFLAG_CANCELEDestá activada. - En la siguiente línea, crea un condicional que verifique si la variable
canceledes verdadera. Si es así, llama a la funcióncancelLastStrokepara quitar el último conjunto de objetosMotionEvent. De lo contrario, llama al métodocurrentPath.addpara agregar el último conjunto de objetosMotionEvent.
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))
}
}
- En la constante
ACTION_CANCEL, observa la funcióncancelLastStroke:
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
MotionEvent.ACTION_CANCEL -> {
// unwanted touch detected
cancelLastStroke()
}
Se implementó el rechazo de la palma. Puedes encontrar el código que funciona en la carpeta palm-rejection.
6. Implementa la latencia baja
En esta sección, reducirás la latencia entre la entrada del usuario y la renderización de la pantalla para mejorar el rendimiento. La latencia tiene varias causas, y una de ellas es la canalización gráfica larga. Reduce la canalización gráfica con la renderización del búfer frontal, que les brinda a los desarrolladores acceso directo al búfer de la pantalla, lo que proporciona excelentes resultados para la escritura a mano y los esbozos.
La clase GLFrontBufferedRenderer que proporciona la biblioteca androidx.graphics se encarga de la renderización del búfer frontal y doble. Optimiza un objeto SurfaceView para una renderización rápida con la función de devolución de llamada onDrawFrontBufferedLayer y la renderización normal con la función de devolución de llamada onDrawDoubleBufferedLayer. La clase GLFrontBufferedRenderer y la interfaz GLFrontBufferedRenderer.Callback funcionan con un tipo de datos proporcionado por el usuario. En este codelab, usarás la clase Segment.
Para comenzar, sigue estos pasos:
- En Android Studio, abre la carpeta
low-latencypara obtener todos los archivos necesarios: - Observa los siguientes archivos nuevos en el proyecto:
- En el archivo
build.gradle, se importó la bibliotecaandroidx.graphicscon la declaraciónimplementation "androidx.graphics:graphics-core:1.0.0-alpha03". - La clase
LowLatencySurfaceViewextiende la claseSurfaceViewpara renderizar el código OpenGL en la pantalla. - La clase
LineRenderercontiene el código OpenGL para renderizar una línea en la pantalla. - La clase
FastRendererpermite una renderización rápida e implementa la interfazGLFrontBufferedRenderer.Callback. También intercepta objetosMotionEvent. - La clase
StylusViewModelcontiene los datos con una interfazLineManager. - La clase
Segmentdefine un segmento de la siguiente manera: x1,y1: coordenadas del primer puntox2,y2: coordenadas del segundo punto
En las siguientes imágenes, se muestra cómo se mueven los datos entre cada clase:

Crea una superficie y un diseño de baja latencia
- En el archivo
MainActivity.kt, busca la funciónonCreatede la claseMainActivity. - En el cuerpo de la función
onCreate, crea un objetoFastRenderery, luego, pasa un objetoviewModel:
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fastRendering = FastRenderer(viewModel)
lifecycleScope.launch {
...
- En el mismo archivo, crea una función
ComposabledeDrawAreaLowLatency. - En el cuerpo de la función, usa la API de
AndroidViewpara unir la vistaLowLatencySurfaceViewy, luego, proporcionar el objetofastRendering:
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)
}
- En la función
onCreatedespués de la funciónComposabledeDivider, agrega la funciónComposabledeDrawAreaLowLatencyal diseño:
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()
}
}
- En el directorio
gl, abre el archivoLowLatencySurfaceView.kty, luego, observa lo siguiente en la claseLowLatencySurfaceView:
- La clase
LowLatencySurfaceViewextiende la claseSurfaceView. Usa el métodoonTouchListenerdel objetofastRenderer. - La interfaz
GLFrontBufferedRenderer.Callbacka través de la clasefastRendererdebe adjuntarse al objetoSurfaceViewcuando se llama a la funciónonAttachedToWindowpara que las devoluciones de llamada se puedan renderizar en la vistaSurfaceView. - La interfaz
GLFrontBufferedRenderer.Callbacka través de la clasefastRendererdebe liberarse cuando se llama a la funciónonDetachedFromWindow.
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()
}
}
Controla objetos MotionEvent con la interfaz de onTouchListener
Para controlar objetos MotionEvent cuando se detecta la constante ACTION_DOWN, sigue estos pasos:
- En el directorio
gl, abre el archivoFastRenderer.kt. - En el cuerpo de la constante
ACTION_DOWN, crea una variablecurrentXque almacene la coordenadaxdel objetoMotionEventy una variablecurrentYque almacene su coordenaday. - Crea una variable
Segmentque almacene un objetoSegmentque acepte dos instancias del parámetrocurrentXy dos instancias del parámetrocurrentYporque es el inicio de la línea. - Llama al método
renderFrontBufferedLayercon un parámetrosegmentpara activar una devolución de llamada en la funciónonDrawFrontBufferedLayer.
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)
}
Para controlar objetos MotionEvent cuando se detecta la constante ACTION_MOVE, sigue estos pasos:
- En el cuerpo de la constante
ACTION_MOVE, crea una variablepreviousXque almacene la variablecurrentXy una variablepreviousYque almacene la variablecurrentY. - Crea una variable
currentXque guarda la coordenadaxactual del objetoMotionEventy una variablecurrentYque guarda la coordenadayactual. - Crea una variable
Segmentque almacene un objetoSegmentque acepte los parámetrospreviousX,previousY,currentXycurrentY. - Llama al método
renderFrontBufferedLayercon un parámetrosegmentpara activar una devolución de llamada en la funciónonDrawFrontBufferedLayery ejecutar el código 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)
}
- Para controlar objetos
MotionEventcuando se detecta la constanteACTION_UP, llama al métodocommitpara activar una llamada en la funciónonDrawDoubleBufferedLayery ejecutar el código OpenGL:
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_UP -> {
frontBufferRenderer?.commit()
}
Implementa las funciones de devolución de llamada GLFrontBufferedRenderer.
En el archivo FastRenderer.kt, las funciones de devolución de llamada onDrawFrontBufferedLayer y onDrawDoubleBufferedLayer ejecutan el código OpenGL. Al comienzo de cada función de devolución de llamada, las siguientes funciones de OpenGL asignan datos de Android al espacio de trabajo de OpenGL:
- La función
GLES20.glViewportdefine el tamaño del rectángulo en el que renderizas la escena. - La función
Matrix.orthoMcalcula la matrizModelViewProjection. - La función
Matrix.multiplyMMrealiza la multiplicación de matrices para transformar los datos de Android en la referencia de OpenGL y proporciona la configuración de la matrizprojection.
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)
Con esa parte del código configurada, puedes enfocarte en el código que realiza la renderización real. La función de devolución de llamada onDrawFrontBufferedLayer renderiza un área pequeña de la pantalla. Proporciona un valor param de tipo Segment para que puedas renderizar un solo segmento con rapidez. La clase LineRenderer es un procesador OpenGL para el pincel que aplica el color y el tamaño de la línea.
Para implementar la función de devolución de llamada onDrawFrontBufferedLayer, sigue estos pasos:
- En el archivo
FastRenderer.kt, busca la función de devolución de llamadaonDrawFrontBufferedLayer. - En el cuerpo de la función de devolución de llamada
onDrawFrontBufferedLayer, llama a la funciónobtainRendererpara obtener la instanciaLineRenderer. - Llama al método
drawLinede la funciónLineRenderercon los siguientes parámetros:
- La matriz
projectioncalculada previamente. - Una lista de objetos
Segment, que es un solo segmento en este caso. - El
colorde la línea.
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())
}
- Ejecuta la app y, luego, observa que puedes dibujar en la pantalla con la latencia mínima. Sin embargo, la app no conservará la línea porque aún debes implementar la función de devolución de llamada
onDrawDoubleBufferedLayer.
Se llama a la función de devolución de llamada onDrawDoubleBufferedLayer después de la función commit para permitir la persistencia de la línea. La devolución de llamada proporciona valores params, que contienen una colección de objetos Segment. Todos los segmentos del búfer frontal se vuelven a reproducir en el búfer doble para mantener la persistencia.
Para implementar la función de devolución de llamada onDrawDoubleBufferedLayer, sigue estos pasos:
- En el archivo
StylusViewModel.kt, busca la claseStylusViewModely, luego, crea una variableopenGlLinesque almacene una lista mutable de objetosSegment:
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) {
- En el archivo
FastRenderer.kt, busca la función de devolución de llamadaonDrawDoubleBufferedLayerde la claseFastRenderer. - En el cuerpo de la función de devolución de llamada
onDrawDoubleBufferedLayer, borra la pantalla con los métodosGLES20.glClearColoryGLES20.glClearpara que se pueda renderizar la escena desde cero y agrega las líneas al objetoviewModelpara que se conserven:
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())
- Crea un bucle
forque itere y renderice cada línea desde el objetoviewModel:
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())
}
}
- Ejecuta la app y, luego, observa que puedes dibujar en la pantalla, y la línea se conserva después de que se activa la constante
ACTION_UP.
7. Implementa la predicción de movimiento
Puedes mejorar aún más la latencia con la biblioteca androidx.input, que analiza el recorrido de la pluma stylus, predice la ubicación del siguiente punto y la inserta para su renderización.
Para configurar la predicción de movimiento, sigue estos pasos:
- En el archivo
app/build.gradle, importa la biblioteca en la sección de dependencias:
app/build.gradle
...
dependencies {
...
implementation"androidx.input:input-motionprediction:1.0.0-beta01"
- Haz clic en File > Sync Project with Gradle Files.
- En la clase
FastRenderingdel archivoFastRendering.kt, declara el objetomotionEventPredictorcomo un atributo:
FastRenderer.kt
import androidx.input.motionprediction.MotionEventPredictor
class FastRenderer( ... ) {
...
private var frontBufferRenderer: GLFrontBufferedRenderer<Segment>? = null
private var motionEventPredictor: MotionEventPredictor? = null
- En la función
attachSurfaceView, inicializa la variablemotionEventPredictor:
FastRenderer.kt
class FastRenderer( ... ) {
...
fun attachSurfaceView(surfaceView: SurfaceView) {
frontBufferRenderer = GLFrontBufferedRenderer(surfaceView, this)
motionEventPredictor = MotionEventPredictor.newInstance(surfaceView)
}
- En la variable
onTouchListener, llama al métodomotionEventPredictor?.recordpara que el objetomotionEventPredictorobtenga datos de movimiento:
FastRendering.kt
class FastRenderer( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
motionEventPredictor?.record(event)
...
when (event?.action) {
El siguiente paso es predecir un objeto MotionEvent con la función predict. Recomendamos predecir cuándo se recibe una constante ACTION_MOVE y después de registrar el objeto MotionEvent. En otras palabras, debes predecir cuándo se realiza un trazo.
- Predice un objeto
MotionEventartificial con el métodopredict - Crea un objeto
Segmentque use las coordenadas x e y previstas y actuales. - Solicita una renderización rápida del segmento previsto con el método
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)
}
}
...
}
Los eventos previstos se insertan para renderizar, lo que mejora la latencia.
- Ejecuta la app y, luego, observa la mejora en la latencia.
Si mejoras la latencia, los usuarios tendrán una experiencia más natural con la pluma stylus.
8. Felicitaciones
¡Felicitaciones! ¡Sabes manejar la pluma stylus como un profesional!
Aprendiste a procesar objetos MotionEvent para extraer información sobre la presión, la orientación y la inclinación. También aprendiste a mejorar la latencia con la implementación de la biblioteca androidx.graphics y la biblioteca androidx.input. Todas estas mejoras implementadas ofrecen una experiencia más orgánica de la pluma stylus.