A stylus permite que os usuários interajam com apps de forma confortável e precisa com precisão ao fazer anotações, desenhar, trabalhar com apps de produtividade, relaxar e se divertir com jogos e apps de entretenimento.
O Android e o ChromeOS oferecem várias APIs para criar uma experiência de stylus excepcional nos apps. A classe MotionEvent
fornece informações sobre a interação do usuário com a tela, incluindo pressão da stylus, orientação, inclinação, passagem do cursor e detecção de palma. As bibliotecas de previsão de movimento e gráficos de baixa latência melhoram a renderização da stylus na tela para fornecer uma experiência natural, como papel e caneta.
MotionEvent
A classe MotionEvent
representa as interações de entrada do usuário, como a posição e o movimento dos ponteiros de toque na tela. Para entrada com stylus, o MotionEvent
também expõe dados de pressão, orientação, inclinação e passagem do cursor.
Dados de eventos
Para acessar dados do MotionEvent
em apps baseados em visualização, configure um onTouchListener:
Kotlin
val onTouchListener = View.OnTouchListener { view, event -> // Process motion event. }
Java
View.OnTouchListener listener = (view, event) -> { // Process motion event. };
O listener recebe objetos MotionEvent
do sistema para que o app possa processá-los.
Um objeto MotionEvent
fornece dados relacionados a estes aspectos de um evento da interface:
- Ações: interação física com o dispositivo, como tocar na tela, mover um ponteiro sobre a superfície da tela, passar o cursor sobre ela
- Ponteiros: identificadores de objetos que interagem com a tela (dedo, stylus, mouse)
- Eixo: tipo de dados — coordenadas x e y, pressão, inclinação, orientação e passar o cursor (distância)
Ações
Para implementar o suporte à stylus, você precisa entender qual ação o usuário está executando.
MotionEvent
fornece uma ampla gama de constantes ACTION
que definem eventos de movimento. As ações mais importantes para a stylus incluem:
Ação | Descrição |
---|---|
ACTION_DOWN ACTION_POINTER_DOWN |
O ponteiro fez contato com a tela. |
ACTION_MOVE | O ponteiro está se movendo na tela. |
ACTION_UP ACTION_POINTER_UP |
O ponteiro não está mais em contato com a tela |
ACTION_CANCEL | Quando o conjunto de movimentos anterior ou atual precisa ser cancelado. |
Seu app pode realizar tarefas como iniciar um novo traço quando a ACTION_DOWN
ocorre, desenhar o traço com ACTION_MOVE,
e finalizá-lo quando ACTION_UP
é acionado.
O conjunto de ações de MotionEvent
de ACTION_DOWN
a ACTION_UP
para um determinado ponteiro é chamado de conjunto de movimentos.
Ponteiros
A maioria das telas é multitoque: o sistema atribui um ponteiro para cada dedo, stylus, mouse ou outro objeto que interaja com a tela. Um índice de ponteiro permite que você receba informações sobre o eixo de um ponteiro específico, por exemplo, a posição do primeiro ou segundo dedo que toca na tela.
Os índices de ponteiro variam de zero ao número de ponteiros retornados por MotionEvent#pointerCount()
menos 1.
Os valores dos eixos dos ponteiros podem ser acessados com o método getAxisValue(axis, pointerIndex)
. Quando o índice de ponteiro é omitido, o sistema retorna o valor para o primeiro ponteiro, o índice zero (0).
Os objetos MotionEvent
contêm informações sobre o tipo de ponteiro em uso. Você pode conferir o tipo de ponteiro iterando os índices deles e chamando o método getToolType(pointerIndex)
.
Para saber mais sobre ponteiros, consulte Processar gestos multitoque.
Entradas da stylus
É possível filtrar as entradas da stylus com TOOL_TYPE_STYLUS
:
Kotlin
val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)
Java
boolean isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex);
A stylus também pode informar que está sendo usada como borracha com TOOL_TYPE_ERASER
:
Kotlin
val isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex)
Java
boolean isEraser = TOOL_TYPE_ERASER == event.getToolType(pointerIndex);
Dados do eixo da stylus
ACTION_DOWN
e ACTION_MOVE
fornecem dados de eixo sobre a stylus, ou seja, coordenadas x e y, pressão, orientação, inclinação e estado de passar o cursor.
Para ativar o acesso a esses dados, a API MotionEvent
fornece getAxisValue(int)
, em que o parâmetro é qualquer um dos identificadores de eixo abaixo:
Eixo | Valor de retorno de getAxisValue() |
---|---|
AXIS_X |
Coordenada X de um evento de movimento. |
AXIS_Y |
Coordenada Y de um evento de movimento. |
AXIS_PRESSURE |
Para touchscreen ou touchpad, a pressão aplicada por um dedo, uma stylus ou outro ponteiro. Para um mouse ou trackball, 1 se o botão principal for pressionado. 0 caso contrário. |
AXIS_ORIENTATION |
Para touchscreen ou touchpad, a orientação de um dedo, uma stylus ou outro ponteiro em relação ao plano vertical do dispositivo. |
AXIS_TILT |
O ângulo de inclinação da stylus em radianos. |
AXIS_DISTANCE |
A distância entre a stylus e a tela. |
Por exemplo, MotionEvent.getAxisValue(AXIS_X)
retorna a coordenada x para o primeiro ponteiro.
Consulte também Processar gestos multitoque.
Posição
Você pode extrair as coordenadas x e y de um ponteiro com estas chamadas:
MotionEvent#getAxisValue(AXIS_X)
ouMotionEvent#getX()
MotionEvent#getAxisValue(AXIS_Y)
ouMotionEvent#getY()
Pressão
Você pode extrair dados da pressão do ponteiro com estas chamadas:
getAxisValue(AXIS_PRESSURE)
ou getPressure()
para o primeiro ponteiro.
O valor da pressão para touchscreens ou touchpads é um valor entre 0 (sem pressão) e 1, mas valores mais altos podem ser retornados dependendo da calibragem da tela.
Orientação
A orientação indica para qual direção a stylus está apontando.
A orientação do ponteiro pode ser extraída usando getAxisValue(AXIS_ORIENTATION)
ou getOrientation()
(para o primeiro ponteiro).
Para uma stylus, a orientação é retornada como um valor radiano entre 0 e pi (𝛑) no sentido horário ou 0 a -pi no sentido anti-horário.
A orientação permite que você implemente um pincel realista. Por exemplo, se a stylus representar um pincel plano, a largura dele dependerá da orientação da stylus.
Inclinação
A inclinação mede a inclinação da stylus em relação à tela.
A inclinação retorna o ângulo positivo da stylus em radianos, em que zero é perpendicular à tela e 𝛑/2 é plano na superfície.
O ângulo de inclinação pode ser extraído usando getAxisValue(AXIS_TILT)
(sem atalho para o primeiro ponteiro).
A inclinação pode ser usada para simular ferramentas reais da melhor forma possível, como imitar o sombreamento com um lápis inclinado.
Passar o cursor
A distância entre a stylus e a tela pode ser extraída com getAxisValue(AXIS_DISTANCE)
. O método retorna um valor de 0,0 (contato com a tela) até valores mais altos à medida que a stylus se afasta da tela. A distância de passagem do cursor entre a tela e a ponta da stylus depende do fabricante da tela e da stylus. Como as implementações podem variar, não confie em valores precisos para a funcionalidade essencial do app.
O recurso de passar o cursor com a stylus pode ser usado para visualizar o tamanho do pincel ou indicar que um botão será selecionado.
Observação: o Compose oferece um conjunto de elementos modificadores para mudar o estado dos elementos da interface:
hoverable
: configura o componente para que seja possível passar o cursor usando eventos de entrada e saída do ponteiro.indication
: renderiza efeitos visuais para o componente quando houver interações.
Rejeição da palma da mão, navegação e entradas indesejadas
Às vezes, telas multitoque podem registrar toques indesejados, por exemplo, quando um usuário apoia naturalmente a mão na tela enquanto está escrevendo à mão. A rejeição da palma é um mecanismo que detecta esse comportamento e informa que o último MotionEvent
definido precisa ser cancelado.
Como resultado, é necessário manter um histórico de entradas do usuário para que os toques indesejados possam ser removidos da tela e as entradas de usuários legítimas possam ser renderizadas novamente.
ACTION_CANCEL e FLAG_CANCELED
Os elementos ACTION_CANCEL
e FLAG_CANCELED
foram criados para informar que o MotionEvent
definido anteriormente precisa ser cancelado no último ACTION_DOWN
para que seja possível, por exemplo, desfazer o último traço de um app de desenho para um determinado ponteiro.
ACTION_CANCEL
Adicionado no Android 1.0 (nível 1 da API)
ACTION_CANCEL
indica que o conjunto anterior de eventos de movimento precisa ser cancelado.
ACTION_CANCEL
é acionado quando uma destas situações é detectada:
- Gestos de navegação
- Rejeição da palma da mão
Quando ACTION_CANCEL
for acionado, identifique o ponteiro ativo com getPointerId(getActionIndex())
. Em seguida, remova o traço criado com esse ponteiro do histórico de entrada e renderize novamente a cena.
FLAG_CANCELED
Adicionado no Android 13 (nível 33 da API)
FLAG_CANCELED
indica que o ponteiro subir foi um toque do usuário não intencional. A flag normalmente é definida quando o usuário toca acidentalmente na tela, por exemplo, segurando o dispositivo ou colocando a palma da mão na tela.
Acesse o valor da flag desta maneira:
Kotlin
val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED
Java
boolean cancel = (event.getFlags() & FLAG_CANCELED) == FLAG_CANCELED;
Se a flag estiver definida, será necessário desfazer o último conjunto de MotionEvent
do último ACTION_DOWN
desse ponteiro.
Como ACTION_CANCEL
, o ponteiro pode ser encontrado com getPointerId(actionIndex)
.
Gestos em tela cheia, de ponta a ponta e de navegação
Se um app estiver em tela cheia e tiver elementos acionáveis próximos à borda, como a tela de um app de desenho ou anotação, deslizar da parte de baixo da tela para mostrar a navegação ou mover o app para o segundo plano pode resultar em uma ação de toque na tela.
Para evitar que os gestos acionem toques indesejados no app, use os encartes e os ACTION_CANCEL
.
Consulte também Rejeição da palma da mão, navegação e entradas indesejadas acima.
Use o método setSystemBarsBehavior()
e BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
do WindowInsetsController
para evitar que os gestos de navegação causem eventos de toque indesejados:
Kotlin
// Configure the behavior of the hidden system bars. windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
Java
// Configure the behavior of the hidden system bars. windowInsetsController.setSystemBarsBehavior( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE );
Para saber mais sobre o encarte e o gerenciamento de gestos, consulte:
- Ocultar barras de sistema para o modo imersivo
- Garantir a compatibilidade com a navegação por gestos
- Mostrar conteúdo de ponta a ponta no app
Baixa latência
Latência é o tempo exigido pelo hardware, pelo sistema e pelo aplicativo para processar e renderizar a entrada do usuário.
Latência = processamento de entrada de hardware e de SO + processamento de apps + composição de sistemas + renderização de hardware
Origem da latência
- Registro da stylus com touchscreen (hardware): conexão sem fio inicial quando a stylus e o SO se comunicam para serem registrados e sincronizados.
- Taxa de amostragem de toque (hardware): o número de vezes por segundo que uma tela touchscreen verifica se um ponteiro está tocando a superfície, variando de 60 a 1.000 Hz.
- Processamento de entrada (app): aplicação de cores, efeitos gráficos e transformação na entrada do usuário.
- Renderização gráfica (SO + hardware): troca de buffer, processamento de hardware.
Gráficos de baixa latência
A biblioteca de gráficos de baixa latência do Jetpack reduz o tempo de processamento entre a entrada do usuário e a renderização na tela.
A biblioteca reduz o tempo de processamento ao evitar a renderização de vários buffers e aproveitar uma técnica de renderização de buffer frontal, o que significa escrever diretamente na tela.
Renderização do buffer frontal
O buffer frontal é a memória que a tela usa para renderização. Ele é a forma mais rápida de apps renderizarem informações na tela. A biblioteca de baixa latência permite que os apps sejam renderizados diretamente no buffer frontal. Isso melhora o desempenho impedindo a troca do buffer, o que pode acontecer na renderização normal de vários buffers ou na renderização de buffer duplo (o caso mais comum).
Embora a renderização do buffer frontal seja uma ótima técnica para renderizar uma pequena área da tela, ela não foi projetada para ser usada para atualizar toda a tela. Com a renderização do buffer frontal, o app está renderizando o conteúdo em um buffer do qual a tela está lendo. Como resultado, há a possibilidade de renderizar artefatos ou da tela romper (confira abaixo).
A biblioteca de baixa latência está disponível no Android 10 (nível 29 da API) e versões mais recentes e em dispositivos ChromeOS que executam o Android 10 (nível 29 da API) e versões mais recentes.
Dependências
A biblioteca de baixa latência fornece os componentes para a implementação da renderização do buffer frontal. A biblioteca é adicionada como uma dependência no arquivo build.gradle
do módulo do app:
dependencies {
implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}
Callbacks de GLFrontBufferRenderer
A biblioteca de baixa latência inclui a interface GLFrontBufferRenderer.Callback
, que define estes métodos:
A biblioteca de baixa latência não opina sobre os tipos de dados usados com o GLFrontBufferRenderer
.
No entanto, a biblioteca processa os dados como um fluxo de centenas de pontos de dados. Projete seus dados para otimizar o uso e a alocação de memória.
Callbacks
Para ativar os callbacks de renderização, implemente o GLFrontBufferedRenderer.Callback
e substitua onDrawFrontBufferedLayer()
e onDrawDoubleBufferedLayer()
. O GLFrontBufferedRenderer
usa os callbacks para renderizar seus dados da maneira mais otimizada possível.
Kotlin
val callback = object: GLFrontBufferedRenderer.Callback<DATA_TYPE> { override fun onDrawFrontBufferedLayer( eglManager: EGLManager, bufferInfo: BufferInfo, transform: FloatArray, param: DATA_TYPE ) { // OpenGL for front buffer, short, affecting small area of the screen. } override fun onDrawMultiDoubleBufferedLayer( eglManager: EGLManager, bufferInfo: BufferInfo, transform: FloatArray, params: Collection<DATA_TYPE> ) { // OpenGL full scene rendering. } }
Java
GLFrontBufferedRenderer.Callback<DATA_TYPE> callbacks = new GLFrontBufferedRenderer.Callback<DATA_TYPE>() { @Override public void onDrawFrontBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, DATA_TYPE data_type) { // OpenGL for front buffer, short, affecting small area of the screen. } @Override public void onDrawDoubleBufferedLayer(@NonNull EGLManager eglManager, @NonNull BufferInfo bufferInfo, @NonNull float[] transform, @NonNull Collection<? extends DATA_TYPE> collection) { // OpenGL full scene rendering. } };
Declarar uma instância do GLFrontBufferedRenderer
Prepare o GLFrontBufferedRenderer
fornecendo a SurfaceView
e os callbacks criados anteriormente. O GLFrontBufferedRenderer
otimiza a renderização para o buffer duplo e frontal usando seus callbacks:
Kotlin
var glFrontBufferRenderer = GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks)
Java
GLFrontBufferedRenderer<DATA_TYPE> glFrontBufferRenderer = new GLFrontBufferedRenderer<DATA_TYPE>(surfaceView, callbacks);
Renderização
A renderização do buffer frontal começa quando você chama o método renderFrontBufferedLayer()
, que aciona o callback onDrawFrontBufferedLayer()
.
A renderização do buffer duplo é retomada quando você chama a função commit()
, que aciona o callback onDrawMultiDoubleBufferedLayer()
.
No exemplo a seguir, o processo é renderizado no buffer frontal (renderização rápida) quando o usuário começa a desenhar na tela (ACTION_DOWN
) e move o ponteiro (ACTION_MOVE
). O processo é renderizado no buffer duplo quando o ponteiro sai da superfície da tela (ACTION_UP
).
Você pode usar requestUnbufferedDispatch()
para pedir que o sistema de entrada não agrupe eventos de movimento em lote, mas os entregue assim que estiverem disponíveis:
Kotlin
when (motionEvent.action) { MotionEvent.ACTION_DOWN -> { // Deliver input events as soon as they arrive. view.requestUnbufferedDispatch(motionEvent) // Pointer is in contact with the screen. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE) } MotionEvent.ACTION_MOVE -> { // Pointer is moving. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE) } MotionEvent.ACTION_UP -> { // Pointer is not in contact in the screen. glFrontBufferRenderer.commit() } MotionEvent.CANCEL -> { // Cancel front buffer; remove last motion set from the screen. glFrontBufferRenderer.cancel() } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_DOWN: { // Deliver input events as soon as they arrive. surfaceView.requestUnbufferedDispatch(motionEvent); // Pointer is in contact with the screen. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_MOVE: { // Pointer is moving. glFrontBufferRenderer.renderFrontBufferedLayer(DATA_TYPE); } break; case MotionEvent.ACTION_UP: { // Pointer is not in contact in the screen. glFrontBufferRenderer.commit(); } break; case MotionEvent.ACTION_CANCEL: { // Cancel front buffer; remove last motion set from the screen. glFrontBufferRenderer.cancel(); } break; }
O que fazer e o que não fazer
O que fazer
Renderize apenas pequenas partes da tela, escrita à mão, desenhos, esboços.
O que não fazer
Não renderize atualizações em tela cheia, com efeito panorâmico ou zoom. Isso pode causar rupturas.
Ruptura
A ruptura acontece quando a tela é atualizada ao mesmo tempo em que o buffer está sendo modificado. Uma parte da tela mostra dados novos, enquanto outra mostra dados antigos.
Previsão de movimento
A biblioteca de previsão de movimento do Jetpack reduz a latência percebida ao estimar o caminho do traço do usuário e fornecer pontos artificiais temporários ao renderizador.
A biblioteca de previsão de movimento recebe entradas reais do usuário como objetos MotionEvent
. Os objetos contêm informações sobre as coordenadas x e y, a pressão e o tempo, que são aproveitadas pelo previsor de movimento para prever objetos MotionEvent
no futuro.
Os objetos MotionEvent
previstos são apenas estimativas. Os eventos previstos podem reduzir a latência percebida, mas os dados previstos precisam ser substituídos por dados MotionEvent
reais depois de recebidos.
A biblioteca de previsão de movimento está disponível no Android 4.4 (nível 19 da API) e versões mais recentes e em dispositivos ChromeOS com o Android 9 (nível 28 da API) e versões mais recentes.
Dependências
A biblioteca de previsão de movimento fornece a implementação da previsão. A biblioteca é adicionada como uma dependência no arquivo build.gradle
do módulo do app:
dependencies {
implementation "androidx.input:input-motionprediction:1.0.0-beta01"
}
Implementação
A biblioteca de previsão de movimento inclui a interface MotionEventPredictor
, que define os métodos abaixo:
record()
: armazena objetosMotionEvent
como um registro das ações do usuário.predict()
: retorna umMotionEvent
previsto.
Declare uma instância de MotionEventPredictor
.
Kotlin
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Java
MotionEventPredictor motionEventPredictor = MotionEventPredictor.newInstance(surfaceView);
Alimentar o previsor com dados
Kotlin
motionEventPredictor.record(motionEvent)
Java
motionEventPredictor.record(motionEvent);
Previsão
Kotlin
when (motionEvent.action) { MotionEvent.ACTION_MOVE -> { val predictedMotionEvent = motionEventPredictor?.predict() if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } }
Java
switch (motionEvent.getAction()) { case MotionEvent.ACTION_MOVE: { MotionEvent predictedMotionEvent = motionEventPredictor.predict(); if(predictedMotionEvent != null) { // use predicted MotionEvent to inject a new artificial point } } break; }
O que fazer e o que não fazer na previsão de movimentos
O que fazer
Remova os pontos de previsão quando um novo ponto previsto for adicionado.
O que não fazer
Não use pontos de previsão para a renderização final.
Apps de anotações
O ChromeOS permite que o app declare algumas ações de anotação.
Para registrar um app como um app de anotações no ChromeOS, consulte Compatibilidade de entrada.
Para registrar um app como um app de anotações no Android, consulte Criar um app de anotações.
A intent ACTION_CREATE_NOTE
, que permite que o app inicie uma atividade de anotação na tela de bloqueio, foi lançada no Android 14 (nível 34 da API).
Reconhecimento de tinta digital com o Kit de ML
Com o reconhecimento de tinta digital do Kit de ML, seu app pode reconhecer texto escrito à mão em uma superfície digital em centenas de idiomas. Você também pode classificar os esboços.
O Kit de ML fornece a classe Ink.Stroke.Builder
para criar objetos Ink
que podem ser processados por modelos de machine learning para converter a escrita à mão em texto.
Além do reconhecimento de escrita manual, o modelo é capaz de reconhecer gestos, como excluir e círcular.
Consulte Reconhecimento de tinta digital para saber mais.