Recursos avançados da stylus

O Android e o ChromeOS oferecem uma variedade de APIs para ajudar você a criar apps que ofereçam uma experiência excepcional com a stylus. A Exposição da classe MotionEvent informações sobre a interação da stylus com a tela, incluindo a pressão da stylus; orientação, inclinação, passagem do cursor e detecção de palma. Gráficos e movimento de baixa latência as bibliotecas de previsão melhoram a renderização na tela da stylus para fornecer uma 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, MotionEvent também expõe dados de pressão, orientação, inclinação e passagem do cursor.

Dados de eventos

如需访问 MotionEvent 数据,请为组件添加 pointerInput 修饰符:

@Composable
fun Greeting() {
    Text(
        text = "Hello, Android!", textAlign = TextAlign.Center, style = TextStyle(fontSize = 5.em),
        modifier = Modifier
            .pointerInput(Unit) {
                awaitEachGesture {
                    while (true) {
                        val event = awaitPointerEvent()
                        event.changes.forEach { println(it) }
                    }
                }
            },
    )
}

Um objeto MotionEvent fornece dados relacionados aos seguintes aspectos de uma interface. evento:

  • Ações: interação física com o dispositivo, como tocar na tela mover um ponteiro sobre a superfície da tela, passar o ponteiro sobre a tela superfície
  • 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á realizando desempenho.

O MotionEvent fornece uma ampla variedade de constantes ACTION que definem o movimento. eventos. 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.

O app pode realizar tarefas, como iniciar um novo traço, quando ACTION_DOWN desenhando o traço com ACTION_MOVE, e terminando o traço quando ACTION_UP é acionado.

O conjunto de ações MotionEvent de ACTION_DOWN a ACTION_UP para um determinado é chamado de conjunto de movimentos.

Ponteiros

A maioria das telas é multitoque: o sistema atribui um ponteiro para cada dedo, stylus, mouse ou outro objeto apontando interagindo com a tela. Um ponteiro índice permite que você obtenha informações sobre o eixo de um ponteiro específico, como o na posição do primeiro dedo ou do segundo tocando 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 do primeiro ponteiro zero (0).

Os objetos MotionEvent contêm informações sobre o tipo de ponteiro em uso. Você pode obter o tipo de ponteiro iterando os índices deles e chamando as getToolType(pointerIndex) .

Para saber mais sobre ponteiros, consulte Processar multitoques gestos.

Entradas da stylus

É possível filtrar as entradas da stylus com TOOL_TYPE_STYLUS:

val isStylus = TOOL_TYPE_STYLUS == event.getToolType(pointerIndex)

A stylus também pode informar que está sendo usada como borracha com TOOL_TYPE_ERASER:

val 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, x e coordenadas y, pressão, orientação, inclinação e passar o cursor.

Para permitir o acesso a esses dados, a API MotionEvent fornece getAxisValue(int), em que o parâmetro é qualquer um dos seguintes identificadores de eixo:

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 no primeiro ponteiro.

Consulte também Processar multitoque gestos.

Cargo

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 recuperar a pressão do ponteiro com MotionEvent#getAxisValue(AXIS_PRESSURE) ou, para o primeiro ponteiro, MotionEvent#getPressure().

O valor da pressão para touchscreens ou touchpads é um valor entre 0 (sem pressão) e 1, mas valores maiores podem ser retornados dependendo da tela calibragem.

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 (π) 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 o a stylus representa um pincel plano, a largura dele depende do 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, onde zero é perpendicular à tela e π/2 é plano na superfície.

O ângulo de inclinação pode ser recuperado usando getAxisValue(AXIS_TILT) (sem atalho para o primeiro ponteiro).

A inclinação pode ser usada para reproduzir ferramentas da vida real o mais próximo possível, como imitando 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 cursor

A distância entre a stylus e a tela pode ser obtida com getAxisValue(AXIS_DISTANCE): O método retorna um valor de 0,0 (contato com da tela) para valores mais altos à medida que a stylus se afasta da tela. Passar o cursor a distância 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 uma 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 modificadores que afetam o estado interativo 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 o usuário apoia a mão naturalmente na tela enquanto escreve à mão. A rejeição da palma é um mecanismo que detecta esse comportamento e notifica você de que o último MotionEvent definido precisa ser cancelado.

Como resultado, você precisa manter um histórico de entradas do usuário para que os toques indesejados podem ser removidas da tela e as entradas legítimas do usuário podem ser renderizado novamente.

ACTION_CANCEL e FLAG_CANCELED

ACTION_CANCEL e FLAG_CANCELED estão para informar que o conjunto MotionEvent anterior deve ser cancelado no último ACTION_DOWN para que você possa, por exemplo, desfazer o último 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 sinalização é normalmente definidos quando o usuário toca acidentalmente na tela, por exemplo, segurando do dispositivo ou colocando a palma da mão sobre a tela.

Acesse o valor da flag desta maneira:

val cancel = (event.flags and FLAG_CANCELED) == FLAG_CANCELED

Se a flag estiver definida, será necessário desfazer o último MotionEvent definido, do último ACTION_DOWN a partir deste 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 de navegação em tela cheia, de ponta a ponta e de ponta a ponta

Se um app estiver em tela cheia e tiver elementos acionáveis próximos à borda, como o tela de um aplicativo de desenho ou anotação, deslizando da parte inferior da tela para exibir a navegação ou mover o app para o segundo plano pode resultar em uma toque indesejado 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, você pode: a vantagem dos encartes e ACTION_CANCEL

Consulte também Rejeição da palma da mão, navegação e entradas indesejadas nesta seção.

Use o setSystemBarsBehavior() e BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE de WindowInsetsController para evitar que gestos de navegação causem eventos de toque indesejados:

// Configure the behavior of the hidden system bars.
windowInsetsController.systemBarsBehavior =
    WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

Para saber mais sobre o inset e o gerenciamento de gestos, consulte:

Baixa latência

Latência é o tempo exigido pelo hardware, sistema e 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

  • Registrando a stylus com tela 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 em 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 com vários buffers e aproveitando uma técnica de renderização de buffer frontal, o que significa escrever diretamente para na tela.

Renderização do buffer frontal

O buffer frontal é a memória que a tela usa para renderização. É o lugar mais próximo podem começar a desenhar diretamente na tela. A biblioteca de baixa latência permite 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 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 do ele não foi projetado para atualizar toda a tela. Com renderização do buffer frontal, o aplicativo está renderizando conteúdo em um buffer do qual que a tela está lendo. Assim, existe a possibilidade de renderizar artefatos ou rompimento (veja 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 com 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 renderização do buffer frontal implementação. A biblioteca é adicionada como uma dependência no módulo do app. Arquivo build.gradle:

dependencies {
    implementation "androidx.graphics:graphics-core:1.0.0-alpha03"
}

Callbacks de GLFrontBufferRenderer

A biblioteca de baixa latência inclui GLFrontBufferRenderer.Callback que define os seguintes métodos:

A biblioteca de baixa latência não opina sobre o tipo de dados que você usa com GLFrontBufferRenderer:

No entanto, a biblioteca processa os dados como um fluxo de centenas de pontos de dados; Por isso, projete seus dados para otimizar o uso e a alocação de memória.

Callbacks

Para ativar os callbacks de renderização, implemente GLFrontBufferedRenderer.Callback e substituir onDrawFrontBufferedLayer() e onDrawDoubleBufferedLayer(). O GLFrontBufferedRenderer usa os callbacks para renderizar seus dados da maneira mais otimizada possível.

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.
   }
}
Declarar uma instância do GLFrontBufferedRenderer

Prepare a GLFrontBufferedRenderer fornecendo os SurfaceView e os callbacks criados anteriormente. O GLFrontBufferedRenderer otimiza a renderização ao buffer duplo e frontal usando seus callbacks:

var glFrontBufferRenderer = 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 o método commit() que aciona o callback onDrawMultiDoubleBufferedLayer().

No exemplo a seguir, o processo é renderizado no buffer frontal (rápido renderização) quando o usuário começa a desenhar na tela (ACTION_DOWN) e se move o ponteiro ao redor (ACTION_MOVE). O processo é renderizado no buffer duplo quando o ponteiro sai da superfície da tela (ACTION_UP).

Você pode usar requestUnbufferedDispatch() solicitar que o sistema de entrada não agrupe eventos de movimento em lote, mas sim entregue assim que estiverem disponíveis:

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()
   }
}

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 enquanto o buffer está sendo modificados ao mesmo tempo. 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

Previsão de movimento do Jetpack biblioteca reduz latência percebida ao estimar o caminho do traço do usuário e fornecer dados artificial para o 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, pressão e tempo. que são aproveitadas pelo preditor de movimento para prever MotionEvent futuros. objetos.

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 pelo MotionEvent real dados assim que eles são recebidos.

A biblioteca de previsão de movimento está disponível no Android 4.4 (API de nível 19) 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 é 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 as MotionEventPredictor que define os seguintes métodos:

  • record(): Armazena objetos MotionEvent como um registro das ações do usuário
  • predict(): Retorna um MotionEvent previsto
Declare uma instância de MotionEventPredictor.
var motionEventPredictor = MotionEventPredictor.newInstance(view)
Alimentar o previsor com dados
motionEventPredictor.record(motionEvent)
Previsão

when (motionEvent.action) {
   MotionEvent.ACTION_MOVE -> {
       val predictedMotionEvent = motionEventPredictor?.predict()
       if(predictedMotionEvent != null) {
            // use predicted MotionEvent to inject a new artificial point
       }
   }
}

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 sendo de anotações no ChromeOS, consulte Entrada compatibilidade.

Para registrar um app como sendo de anotações no Android, consulte Criar anotações app.

O Android 14 (nível 34 da API) lançou a ACTION_CREATE_NOTE que permite que o app inicie uma atividade de anotação na fechadura. tela.

Reconhecimento de tinta digital com o Kit de ML

Com a tinta digital do Kit de ML reconhecimento de marca, seu app consegue reconhecer texto escrito à mão em uma plataforma digital em centenas de idiomas. Você também pode classificar os esboços.

O kit de ML fornece Ink.Stroke.Builder para criar objetos Ink que podem ser processados por modelos de machine learning para converter escrita à mão em texto.

Além do reconhecimento de escrita manual, o modelo é capaz de reconhecer gestos, como "Delete" e "Circle".

Consulte Tinta digital reconhecimento para saber mais.

Outros recursos

Guias do desenvolvedor

Codelabs