Suporte a recursos avançados da stylus

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:

Stylus desenhando na tela com as coordenadas x e y mapeadas.
Figura 1. Coordenadas X e Y da tela de um ponteiro da stylus.

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.

Traço da stylus que representa um contínuo de pressão baixa para alta. O traço é estreito e sutil à esquerda, indicando baixa pressão. O traço vai ficando mais largo e mais escuro da esquerda para a direita até chegar à borda da tela, atingindo a maior largura e cor mais escura, indicando a maior pressão.
Figura 2. Representação de pressão: baixa à esquerda, alta à direita.

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.

Figura 3. Stylus apontando para a esquerda a -0,57 radiano.

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.

Stylus inclinada cerca de 40 graus em relação à superfície da tela.
Figura 4. Stylus inclinada cerca de 0,785 radianos ou 45 graus em relação à perpendicular.

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.

Figura 5. Stylus com o cursor passando sobre uma tela. O app reage mesmo que a stylus não toque na superfície da tela.

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).

Figura 6. O traço da stylus e o toque da palma da mão criam conjuntos de MotionEvent. O toque da palma foi cancelado, e a tela foi renderizada novamente.

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.

Figura 7. Gesto de deslizar para mover um app para o segundo plano.

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:

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

A latência faz com que o traço renderizado fique para trás em relação à posição da stylus. A lacuna entre o traço renderizado e a posição da stylus representa a latência.
Figura 8. A latência faz com que o traço renderizado fique para trás em relação à posição da stylus.

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).

O app escreve no buffer da tela e lê do buffer da tela.
Figura 9. Renderização do buffer frontal.
O app escreve no buffer múltiplo, e troca pelo buffer de tela. O app lê diretamente do buffer da tela.
Figura 10. Renderização com vários buffers.

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.

As partes de cima e de baixo da imagem do Android estão desalinhadas devido a rupturas da tela ao atualizar.
Figura 11. A ruptura ocorre quando a tela é atualizada de cima para baixo.

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.

A latência faz com que o traço renderizado fique para trás em relação à posição da stylus. A lacuna entre o traço e a stylus é preenchida com pontos de previsão. A lacuna restante é a latência percebida.
Figura 12. Latência reduzida pela previsão de movimento.

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 objetos MotionEvent como um registro das ações do usuário.
  • predict(): retorna um MotionEvent 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.

Outros recursos

Guias do desenvolvedor

Codelabs