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
MotionEvent
para 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 pastastart
contém o código inicial e a pastaend
conté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.stylus
e clique duas vezes emMainActivity
. O arquivoMainActivity.kt
será aberto. - Na classe
MainActivity
, observe as funçõesComposable
StylusVisualization
eDrawArea
. Nesta seção, você se concentra na funçãoComposable
DrawArea
.
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.kt
e pressioneEnter
(oureturn
no macOS). - No arquivo
StylusState.kt
, crie a classe de dadosStylusState
e 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 classeMainActivity
e 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
StylusViewModel
do arquivoStylusViewModel.kt
, adicione uma funçãocreatePath
. - Crie uma variável
path
do tipoPath
com o construtorPath()
. - Crie um loop
for
em que você itera para cada ponto de dados na variávelcurrentPath
. - Se o ponto de dados for do tipo
START
, chame o métodomoveTo
para iniciar uma linha nas coordenadasx
ey
especificadas. - Caso contrário, chame o método
lineTo
com as coordenadasx
ey
do 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
MotionEvent
na 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_stylusState
do tipoMutableStateFlow
da classeStylusState
e uma variávelstylusState
do tipoStateFlow
da classeStylusState
. A variável_stylusState
é modificada sempre que o estado da stylus é modificado na classeStylusViewModel
e 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
requestRendering
que 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çãorequestRendering
com um parâmetroStylusState
. - No parâmetro
StylusState
, recupere os valores de inclinação, pressão e orientação da variávelmotionEvent
e 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.onCreate
da funçãoonCreate
e 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
DrawArea
Composable
, adicione o modificadorpointerInteropFilter
à funçãoCanvas
Composable
para fornecer objetosMotionEvent
.
- Envie o objeto
MotionEvent
para a funçãoprocessMotionEvent
do 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
drawPath
com o atributostylusState
path
e 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çãoComposable
StylusVisualization
e use as informações do objeto de fluxoStylusState
para 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.kt
para 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 à classeStylusViewModel
e crie uma funçãocancelLastStroke
que encontra o índice do último ponto de dadosSTART
e 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ávelcanceled
que verifica se a versão atual do SDK é o Android 13 ou mais recente e se a constanteFLAG_CANCELED
está ativada. - Na próxima linha, crie uma condicional para verificar se a variável
canceled
é verdadeira. Nesse caso, chame a funçãocancelLastStroke
para remover o último conjunto de objetosMotionEvent
. Caso contrário, chame o métodocurrentPath.add
para 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-latency
para receber todos os arquivos necessários: - Observe os seguintes arquivos novos no projeto:
- No arquivo
build.gradle
, a bibliotecaandroidx.graphics
foi importada com a declaraçãoimplementation "androidx.graphics:graphics-core:1.0.0-alpha03"
. - A classe
LowLatencySurfaceView
estende a classeSurfaceView
para renderizar o código OpenGL na tela. - A classe
LineRenderer
contém o código do OpenGL para renderizar uma linha na tela. - A classe
FastRenderer
permite a renderização rápida e implementa a interfaceGLFrontBufferedRenderer.Callback
. Ela também intercepta objetosMotionEvent
. - A classe
StylusViewModel
contém os pontos de dados com uma interfaceLineManager
. - A classe
Segment
define 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çãoonCreate
da classeMainActivity
. - No corpo da função
onCreate
, crie um objetoFastRenderer
e 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
DrawAreaLowLatency
Composable
. - No corpo da função, use a API
AndroidView
para unir a visualizaçãoLowLatencySurfaceView
e 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 doComposable
Divider
, adicione oComposable
DrawAreaLowLatency
ao 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.kt
e observe o seguinte na classeLowLatencySurfaceView
:
- A classe
LowLatencySurfaceView
estende a classeSurfaceView
. Ela usa o métodoonTouchListener
do objetofastRenderer
. - A interface
GLFrontBufferedRenderer.Callback
pela da classefastRenderer
precisa ser anexada ao objetoSurfaceView
quando a funçãoonAttachedToWindow
é chamada. Assim, os callbacks podem ser renderizados na visualizaçãoSurfaceView
. - A interface
GLFrontBufferedRenderer.Callback
pela da classefastRenderer
precisa 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ávelcurrentX
que armazene a coordenadax
do objetoMotionEvent
e uma variávelcurrentY
que armazene a coordenaday
. - Crie uma variável
Segment
que armazene um objetoSegment
que aceite duas instâncias do parâmetrocurrentX
e duas instâncias do parâmetrocurrentY
por ser o início da linha. - Chame o método
renderFrontBufferedLayer
com um parâmetrosegment
para 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ávelpreviousX
que armazene a variávelcurrentX
e outrapreviousY
que armazenecurrentY
. - Crie uma variável
currentX
que salve a coordenadax
atual do objetoMotionEvent
e uma variávelcurrentY
que salve a coordenaday
atual. - Crie uma variável
Segment
que armazene um objetoSegment
que aceite os parâmetrospreviousX
,previousY
,currentX
ecurrentY
. - Chame o método
renderFrontBufferedLayer
com um parâmetrosegment
para acionar um callback na funçãoonDrawFrontBufferedLayer
e 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
MotionEvent
quando a constanteACTION_UP
for detectada, chame o métodocommit
para acionar uma chamada na funçãoonDrawDoubleBufferedLayer
e 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.glViewport
define o tamanho do retângulo em que você renderiza o cenário. - A função
Matrix.orthoM
calcula a matrizModelViewProjection
. - A função
Matrix.multiplyMM
executa 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çãoobtainRenderer
para receber a instânciaLineRenderer
. - Chame o método
drawLine
da funçãoLineRenderer
com os seguintes parâmetros:
- A matriz
projection
calculada anteriormente. - Uma lista de objetos
Segment
, que é um único segmento nesse caso. - A
color
da 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 classeStylusViewModel
e crie uma variávelopenGlLines
que 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 callbackonDrawDoubleBufferedLayer
da classeFastRenderer
. - No corpo da função de callback
onDrawDoubleBufferedLayer
, limpe a tela com os métodosGLES20.glClearColor
eGLES20.glClear
para que o cenário possa ser renderizado do zero e adicione as linhas ao objetoviewModel
para 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
for
que 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
FastRendering
do arquivoFastRendering.kt
, declare o objetomotionEventPredictor
como 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?.record
para que o objetomotionEventPredictor
receba 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
MotionEvent
artificial com o métodopredict
. - Crie um objeto
Segment
que 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á aos usuários da stylus uma experiência mais natural.
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)