1. Antes de começar
Uma stylus é uma ferramenta de caneta que ajuda os usuários a realizar tarefas precisas. Neste codelab, você vai aprender a implementar experiências orgânicas da stylus com as bibliotecas android.os e androidx. Você também vai aprender a usar a classe MotionEvent para oferecer suporte a pressão, inclinação e orientação e rejeição de palmas para evitar toques indesejados. Além disso, você vai aprender a reduzir a latência da stylus com previsão de movimento e gráficos de baixa latência com o OpenGL e a classe SurfaceView.
Pré-requisitos
- Experiência com Kotlin e lambdas.
- Conhecimentos básicos sobre como usar o Android Studio.
- Conhecimentos básicos sobre o Jetpack Compose.
- Noções básicas do OpenGL para gráficos de baixa latência.
O que você vai aprender
- Como usar a classe
MotionEventpara a stylus. - Como implementar os recursos da stylus, incluindo suporte a pressão, inclinação e orientação.
- Como desenhar na classe
Canvas. - Como implementar a previsão de movimento.
- Como renderizar gráficos de baixa latência com o OpenGL e a classe
SurfaceView.
O que é necessário
- Ter a versão mais recente do Android Studio.
- Experiência com a sintaxe do Kotlin, incluindo lambdas.
- Experiência básica com o Compose. Se você não conhece o Compose, conclua o codelab Noções básicas do Jetpack Compose.
- Um dispositivo que ofereça suporte à stylus.
- Uma stylus ativa.
- Git.
2. Acessar o código inicial
Para receber o código que contém os temas e a configuração básica do app inicial, siga estas etapas:
- Clone este repositório do GitHub:
git clone https://github.com/android/large-screen-codelabs
- Abra a pasta
advanced-stylus. A pastastartcontém o código inicial e a pastaendcontém o código da solução.
3. Implementar um app básico de desenho
Primeiro, você cria o layout necessário para um app básico de desenho que permite aos usuários desenhar e mostra os atributos da stylus na tela com a função Canvas Composable. Ela tem a seguinte aparência:

A parte de cima é uma função Composable Canvas onde você desenha a visualização da stylus e mostra os diferentes atributos dela, como orientação, inclinação e pressão. A parte de baixo é outra função Composable Canvas que recebe entrada da stylus e desenha traços simples.
Para implementar o layout básico do app de desenho, siga estas etapas:
- No Android Studio, abra o repositório clonado.
- Clique em
app>java>com.example.styluse clique duas vezes emMainActivity. O arquivoMainActivity.ktserá aberto. - Na classe
MainActivity, observe as funçõesComposableStylusVisualizationeDrawArea. Nesta seção, você se concentra na funçãoComposableDrawArea.
Criar uma classe StylusState
- No mesmo diretório
ui, clique em File > New > Kotlin/Class file. - Na caixa de texto, substitua o marcador Name por
StylusState.kte pressioneEnter(oureturnno macOS). - No arquivo
StylusState.kt, crie a classe de dadosStylusStatee adicione as variáveis da seguinte tabela:
Variável | Tipo | Valor padrão | Descrição |
|
| Um valor de 0 a 1,0. | |
|
| Um valor de radiano de -pi a pi. | |
|
| Um valor de radiano de 0 a pi/2. | |
|
| Armazena linhas renderizadas pela função |
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(),
)

- No arquivo
MainActivity.kt, encontre a classeMainActivitye adicione o estado da stylus com a funçãomutableStateOf():
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())
A classe DrawPoint
A classe DrawPoint armazena dados sobre cada ponto desenhado na tela. Ao vincular esses pontos, você cria linhas. Ela imita o funcionamento do objeto Path.
A classe DrawPoint estende a classe PointF. Ela contém os seguintes dados:
Parâmetros | Tipo | Descrição |
|
| Coordenadas |
|
| Coordenadas |
|
| Tipo de ponto |
Há dois tipos de objetos DrawPoint, que são descritos pelo enum DrawPointType:
Tipo | Descrição |
| Move o início de uma linha para uma posição. |
| Traça uma linha a partir do ponto anterior. |
DrawPoint.kt
import android.graphics.PointF
class DrawPoint(x: Float, y: Float, val type: DrawPointType): PointF(x, y)
Renderizar os pontos de dados em um caminho
Para este app, a classe StylusViewModel contém dados da linha, prepara dados para renderização e executa algumas operações no objeto Path para rejeição de palmas.
- Para armazenar os dados das linhas, na classe
StylusViewModel, crie uma lista mutável de objetosDrawPoint:
StylusViewModel.kt
import androidx.lifecycle.ViewModel
import com.example.stylus.data.DrawPoint
class StylusViewModel : ViewModel() {private var currentPath = mutableListOf<DrawPoint>()
Para renderizar os pontos de dados em um caminho, siga estas etapas:
- Na classe
StylusViewModeldo arquivoStylusViewModel.kt, adicione uma funçãocreatePath. - Crie uma variável
pathdo tipoPathcom o construtorPath(). - Crie um loop
forem que você itera para cada ponto de dados na variávelcurrentPath. - Se o ponto de dados for do tipo
START, chame o métodomoveTopara iniciar uma linha nas coordenadasxeyespecificadas. - Caso contrário, chame o método
lineTocom as coordenadasxeydo ponto de dados para vincular ao ponto anterior. - Retorne o 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() {
}
Processar objetos MotionEvent
Os eventos da stylus vêm por objetos MotionEvent, que fornecem informações sobre a ação realizada e os dados associados a ela, como a posição do ponteiro e a pressão. A tabela a seguir contém algumas constantes do objeto MotionEvent e os respectivos dados, que você pode usar para identificar o que o usuário faz na tela:
Constante | Dados |
| O ponteiro toca na tela. É o início de uma linha na posição informada pelo objeto |
| O ponteiro se move na tela. É a linha desenhada. |
| O ponteiro para de tocar na tela. É o fim da linha. |
| Um toque indesejado foi detectado. O último traço é cancelado. |
Quando o app recebe um novo objeto MotionEvent, a tela precisa renderizar para refletir a nova entrada do usuário.
- Para processar objetos
MotionEventna classeStylusViewModel, crie uma função que reúna as coordenadas da linha:
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
}
Enviar dados para a interface
Para atualizar a classe StylusViewModel para que a interface possa coletar mudanças na classe de dados StylusState, siga estas etapas:
- Na classe
StylusViewModel, crie uma variável_stylusStatedo tipoMutableStateFlowda classeStylusStatee uma variávelstylusStatedo tipoStateFlowda classeStylusState. A variável_stylusStateé modificada sempre que o estado da stylus é modificado na classeStylusViewModele a variávelstylusStateé consumida pela interface na 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
- Crie uma função
requestRenderingque aceite um parâmetro de 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
}
}
- No final da função
processMotionEvent, adicione uma chamada de funçãorequestRenderingcom um parâmetroStylusState. - No parâmetro
StylusState, recupere os valores de inclinação, pressão e orientação da variávelmotionEvente crie o caminho com uma funçãocreatePath(). Isso aciona um evento de fluxo, que você vai conectar na interface mais tarde.
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()
)
)
Vincular a interface à classe StylusViewModel
- Na classe
MainActivity, encontre a funçãosuper.onCreateda funçãoonCreatee adicione a coleção de estados. Para saber mais sobre a coleta de estados, consulte Como coletar fluxos de modo ciente do ciclo de vida (link em inglês).
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()
}
}
Agora, sempre que a classe StylusViewModel postar um novo estado StylusState, a atividade vai receber e o novo objeto StylusState vai atualizar a variável stylusState da classe local MainActivity.
- No corpo da função
DrawAreaComposable, adicione o modificadorpointerInteropFilterà funçãoCanvasComposablepara fornecer objetosMotionEvent.
- Envie o objeto
MotionEventpara a funçãoprocessMotionEventdo StylusViewModel para processamento:
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)
}
) {
}
}
- Chame a função
drawPathcom o atributostylusStatepathe forneça uma cor e um estilo de traço.
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
)
}
}
}
- Execute o app e observe que você pode desenhar na tela.
4. Implementar suporte para pressão, orientação e inclinação
Na seção anterior, você aprendeu a recuperar informações da stylus de objetos MotionEvent, como pressão, orientação e inclinação.
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT),
pressure = motionEvent.pressure,
orientation = motionEvent.orientation,
No entanto, esse atalho funciona apenas para o primeiro ponteiro. Quando o recurso multitoque é detectado, vários ponteiros são detectados, e esse atalho só retorna o valor do primeiro ponteiro ou do primeiro ponteiro na tela. Para solicitar dados sobre um ponteiro específico, use o parâmetro pointerIndex:
StylusViewModel.kt
tilt = motionEvent.getAxisValue(MotionEvent.AXIS_TILT, pointerIndex),
pressure = motionEvent.getPressure(pointerIndex),
orientation = motionEvent.getOrientation(pointerIndex)
Para saber mais sobre ponteiros e multitoque, consulte Gerenciar gestos multitoque.
Adicionar visualização de pressão, orientação e inclinação
- No arquivo
MainActivity.kt, encontre a funçãoComposableStylusVisualizatione use as informações do objeto de fluxoStylusStatepara renderizar a visualização:
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)
}
}
}
- Execute o app. Observe três indicadores na parte de cima da tela que indicam orientação, pressão e inclinação.
- Rabisque na tela com a stylus e observe como cada visualização reage com suas entradas.

- Inspecione o arquivo
StylusVisualization.ktpara entender como cada visualização é construída.
5. Implementar rejeição de palmas
A tela pode registrar toques indesejados. Por exemplo, isso acontece quando um usuário apoia a mão naturalmente na tela enquanto escreve à mão.
A rejeição de palmas é um mecanismo que detecta esse comportamento e notifica o desenvolvedor para cancelar o último conjunto de objetos MotionEvent. Um conjunto de objetos MotionEvent começa com a constante ACTION_DOWN.
Isso significa que você precisa manter um histórico das entradas para remover toques indesejados da tela e renderizar novamente as entradas legítimas do usuário. Felizmente, o histórico já está armazenado na classe StylusViewModel na variável currentPath.
O Android oferece a constante ACTION_CANCEL do objeto MotionEvent para informar ao desenvolvedor sobre toques indesejados. Desde o Android 13, o objeto MotionEvent fornece a constante FLAG_CANCELED que precisa ser verificada na constante ACTION_POINTER_UP.
Implementar a função cancelLastStroke
- Para remover um ponto de dados do último
START, volte à classeStylusViewModele crie uma funçãocancelLastStrokeque encontra o índice do último ponto de dadosSTARTe mantém apenas os dados do primeiro ponto até o índice menos um:
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)
}
}
Adicionar as constantes ACTION_CANCEL e FLAG_CANCELED
- No arquivo
StylusViewModel.kt, encontre a funçãoprocessMotionEvent. - Na constante
ACTION_UP, crie uma variávelcanceledque verifica se a versão atual do SDK é o Android 13 ou mais recente e se a constanteFLAG_CANCELEDestá ativada. - Na próxima linha, crie uma condicional para verificar se a variável
canceledé verdadeira. Nesse caso, chame a funçãocancelLastStrokepara remover o último conjunto de objetosMotionEvent. Caso contrário, chame o métodocurrentPath.addpara adicionar o ú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))
}
}
- Na constante
ACTION_CANCEL, observe a funçãocancelLastStroke:
StylusViewModel.kt
...
class StylusViewModel : ViewModel() {
...
fun processMotionEvent(motionEvent: MotionEvent): Boolean {
...
MotionEvent.ACTION_CANCEL -> {
// unwanted touch detected
cancelLastStroke()
}
A rejeição de palmas foi implementada. Você pode encontrar o código em funcionamento na pasta palm-rejection.
6. Implementar baixa latência
Nesta seção, você reduz a latência entre a entrada do usuário e a renderização da tela para melhorar o desempenho. A latência tem várias causas, sendo uma delas o pipeline gráfico longo. Você reduz o pipeline gráfico com a renderização do buffer frontal. A renderização do buffer frontal fornece aos desenvolvedores acesso direto ao buffer de tela, gerando ótimos resultados para escrita à mão e desenho.
A classe GLFrontBufferedRenderer fornecida pela biblioteca androidx.graphics cuida da renderização do buffer frontal e duplicado. Ela otimiza um objeto SurfaceView para renderização rápida com a função de callback onDrawFrontBufferedLayer e a renderização normal com a função de callback onDrawDoubleBufferedLayer. A classe GLFrontBufferedRenderer e a interface GLFrontBufferedRenderer.Callback funcionam com um tipo de dados fornecido pelo usuário. Neste codelab, você vai usar a classe Segment.
Para começar, siga estas etapas:
- No Android Studio, abra a pasta
low-latencypara receber todos os arquivos necessários: - Observe os seguintes arquivos novos no projeto:
- No arquivo
build.gradle, a bibliotecaandroidx.graphicsfoi importada com a declaraçãoimplementation "androidx.graphics:graphics-core:1.0.0-alpha03". - A classe
LowLatencySurfaceViewestende a classeSurfaceViewpara renderizar o código OpenGL na tela. - A classe
LineRenderercontém o código do OpenGL para renderizar uma linha na tela. - A classe
FastRendererpermite a renderização rápida e implementa a interfaceGLFrontBufferedRenderer.Callback. Ela também intercepta objetosMotionEvent. - A classe
StylusViewModelcontém os pontos de dados com uma interfaceLineManager. - A classe
Segmentdefine um segmento da seguinte maneira: x1,y1: coordenadas do primeiro pontox2,y2: coordenadas do segundo ponto
As imagens a seguir mostram como os dados se movem entre cada classe:

Criar uma plataforma e um layout de baixa latência
- No arquivo
MainActivity.kt, encontre a funçãoonCreateda classeMainActivity. - No corpo da função
onCreate, crie um objetoFastRenderere transmita um objetoviewModel:
MainActivity.kt
class MainActivity : ComponentActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fastRendering = FastRenderer(viewModel)
lifecycleScope.launch {
...
- No mesmo arquivo, crie uma função
DrawAreaLowLatencyComposable. - No corpo da função, use a API
AndroidViewpara unir a visualizaçãoLowLatencySurfaceViewe fornecer o 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)
}
- Na função
onCreate, depois doComposableDivider, adicione oComposableDrawAreaLowLatencyao layout:
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()
}
}
- No diretório
gl, abra o arquivoLowLatencySurfaceView.kte observe o seguinte na classeLowLatencySurfaceView:
- A classe
LowLatencySurfaceViewestende a classeSurfaceView. Ela usa o métodoonTouchListenerdo objetofastRenderer. - A interface
GLFrontBufferedRenderer.Callbackpela da classefastRendererprecisa ser anexada ao objetoSurfaceViewquando a funçãoonAttachedToWindowé chamada. Assim, os callbacks podem ser renderizados na visualizaçãoSurfaceView. - A interface
GLFrontBufferedRenderer.Callbackpela da classefastRendererprecisa ser liberada quando a funçãoonDetachedFromWindowé chamada.
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()
}
}
Gerenciar objetos MotionEvent com a interface onTouchListener
Para processar objetos MotionEvent quando a constante ACTION_DOWN for detectada, siga estas etapas:
- No diretório
gl, abra o arquivoFastRenderer.kt. - No corpo da constante
ACTION_DOWN, crie uma variávelcurrentXque armazene a coordenadaxdo objetoMotionEvente uma variávelcurrentYque armazene a coordenaday. - Crie uma variável
Segmentque armazene um objetoSegmentque aceite duas instâncias do parâmetrocurrentXe duas instâncias do parâmetrocurrentYpor ser o início da linha. - Chame o método
renderFrontBufferedLayercom um parâmetrosegmentpara acionar um callback na funçãoonDrawFrontBufferedLayer.
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 processar objetos MotionEvent quando a constante ACTION_MOVE for detectada, siga estas etapas:
- No corpo da constante
ACTION_MOVE, crie uma variávelpreviousXque armazene a variávelcurrentXe outrapreviousYque armazenecurrentY. - Crie uma variável
currentXque salve a coordenadaxatual do objetoMotionEvente uma variávelcurrentYque salve a coordenadayatual. - Crie uma variável
Segmentque armazene um objetoSegmentque aceite os parâmetrospreviousX,previousY,currentXecurrentY. - Chame o método
renderFrontBufferedLayercom um parâmetrosegmentpara acionar um callback na funçãoonDrawFrontBufferedLayere executar o 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 processar objetos
MotionEventquando a constanteACTION_UPfor detectada, chame o métodocommitpara acionar uma chamada na funçãoonDrawDoubleBufferedLayere execute o código OpenGL:
FastRenderer.kt
class FastRenderer ( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
...
MotionEvent.ACTION_UP -> {
frontBufferRenderer?.commit()
}
Implementar as funções de callback de GLFrontBufferedRenderer
No arquivo FastRenderer.kt, as funções de callback onDrawFrontBufferedLayer e onDrawDoubleBufferedLayer executam o código OpenGL. No início de cada função de callback, as seguintes funções do OpenGL mapeiam dados do Android para o espaço de trabalho do OpenGL:
- A função
GLES20.glViewportdefine o tamanho do retângulo em que você renderiza o cenário. - A função
Matrix.orthoMcalcula a matrizModelViewProjection. - A função
Matrix.multiplyMMexecuta a multiplicação de matrizes para transformar os dados do Android em uma referência do OpenGL e fornece a configuração para a 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)
Com essa parte do código configurada para você, é possível se concentrar no código que faz a renderização real. A função de callback onDrawFrontBufferedLayer renderiza uma pequena área da tela. Ele fornece um valor param do tipo Segment para que você possa renderizar um único segmento rapidamente. A classe LineRenderer é um renderizador OpenGL para o pincel que aplica a cor e o tamanho da linha.
Para implementar a função de callback onDrawFrontBufferedLayer, siga estas etapas:
- No arquivo
FastRenderer.kt, encontre a função de callbackonDrawFrontBufferedLayer. - No corpo da função de callback
onDrawFrontBufferedLayer, chame a funçãoobtainRendererpara receber a instânciaLineRenderer. - Chame o método
drawLineda funçãoLineRenderercom os seguintes parâmetros:
- A matriz
projectioncalculada anteriormente. - Uma lista de objetos
Segment, que é um único segmento nesse caso. - A
colorda linha.
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())
}
- Execute o app e observe que você pode desenhar na tela com latência mínima. No entanto, o app não vai manter a linha porque ainda é necessário implementar a função de callback
onDrawDoubleBufferedLayer.
A função de callback onDrawDoubleBufferedLayer é chamada após a função commit para permitir a persistência da linha. O callback fornece valores params, que contêm uma coleção de objetos Segment. Todos os segmentos do buffer frontal são reproduzidos novamente no buffer duplo para persistência.
Para implementar a função de callback onDrawDoubleBufferedLayer, siga estas etapas:
- No arquivo
StylusViewModel.kt, encontre a classeStylusViewModele crie uma variávelopenGlLinesque armazene uma lista mutável 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) {
- No arquivo
FastRenderer.kt, encontre a função de callbackonDrawDoubleBufferedLayerda classeFastRenderer. - No corpo da função de callback
onDrawDoubleBufferedLayer, limpe a tela com os métodosGLES20.glClearColoreGLES20.glClearpara que o cenário possa ser renderizado do zero e adicione as linhas ao objetoviewModelpara persistir:
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())
- Crie uma repetição
forque itera e renderiza cada linha do 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())
}
}
- Execute o app e observe que você pode desenhar na tela. A linha é preservada depois que a constante
ACTION_UPé acionada.
7. Implementar a previsão de movimento
Você pode melhorar ainda mais a latência com a biblioteca androidx.input, que analisa o curso da stylus, prevê o local do próximo ponto e o insere na renderização.
Para configurar a previsão de movimento, siga estas etapas:
- No arquivo
app/build.gradle, importe a biblioteca na seção de dependências:
app/build.gradle
...
dependencies {
...
implementation"androidx.input:input-motionprediction:1.0.0-beta01"
- Clique em File > Sync Project with Gradle Files.
- Na classe
FastRenderingdo arquivoFastRendering.kt, declare o objetomotionEventPredictorcomo um atributo:
FastRenderer.kt
import androidx.input.motionprediction.MotionEventPredictor
class FastRenderer( ... ) {
...
private var frontBufferRenderer: GLFrontBufferedRenderer<Segment>? = null
private var motionEventPredictor: MotionEventPredictor? = null
- Na função
attachSurfaceView, inicialize a variávelmotionEventPredictor:
FastRenderer.kt
class FastRenderer( ... ) {
...
fun attachSurfaceView(surfaceView: SurfaceView) {
frontBufferRenderer = GLFrontBufferedRenderer(surfaceView, this)
motionEventPredictor = MotionEventPredictor.newInstance(surfaceView)
}
- Na variável
onTouchListener, chame o métodomotionEventPredictor?.recordpara que o objetomotionEventPredictorreceba dados de movimento:
FastRendering.kt
class FastRenderer( ... ) {
...
val onTouchListener = View.OnTouchListener { view, event ->
motionEventPredictor?.record(event)
...
when (event?.action) {
A próxima etapa é prever um objeto MotionEvent com a função predict. Recomendamos prever quando uma constante ACTION_MOVE é recebida e depois que o objeto MotionEvent for registrado. Em outras palavras, você vai prever quando um traço está prestes a acontecer.
- Preveja um objeto
MotionEventartificial com o métodopredict. - Crie um objeto
Segmentque use as coordenadas x e y atuais e previstas. - Solicite a renderização rápida do segmento previsto com o 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)
}
}
...
}
Os eventos previstos são inseridos para a renderização, o que melhora a latência.
- Execute o app e observe a latência aprimorada.
Melhorar a latência dará uma experiência mais natural aos usuários da stylus.
8. Parabéns
Parabéns! Você sabe usar a stylus como um profissional.
Você aprendeu a processar objetos MotionEvent para extrair as informações sobre pressão, orientação e inclinação. Você também aprendeu a melhorar a latência implementando as bibliotecas androidx.graphics e androidx.input. Essas melhorias implementadas em conjunto oferecem uma experiência de stylus mais orgânica.
Saiba mais
- Documentação de stylus avançada
- Reconhecimento de tinta digital
- Agrupar chamadas (link em inglês)
- A classe
MotionEvent(link em inglês) - Baixa latência da stylus (link em inglês)