Compatibilidade de entrada em telas grandes

Em dispositivos de tela grande, os usuários costumam interagir com apps usando um teclado, mouse, trackpad, stylus ou gamepad. Para permitir que o app aceite entrada de dispositivos externos, é preciso fazer o seguinte:

  • Teste o suporte básico do teclado, como Ctrl+Z para desfazer, Ctrl+C para copiar e Ctrl+S para salvar. Consulte Processar ações do teclado para conferir uma lista de atalhos de teclado padrão.
  • Testar o suporte avançado do teclado, por exemplo, a tecla Tab e a navegação pelo teclado com as teclas de seta, a tecla Enter para confirmar a entrada de texto e a barra de espaço para reproduzir e pausar em apps de mídia.
  • Teste interações básicas do mouse, incluindo o botão direito do mouse para acessar o menu de contexto, mudanças de ícone ao passar o cursor e eventos de rolagem da roda do mouse ou trackpad em componentes personalizados.
  • Testar dispositivos de entrada específicos do app, como stylus, controles de jogos e controladores MIDI de apps de música.
  • Considere o suporte avançado de entrada que pode destacar o app em ambientes de computador. Por exemplo, poder usar o touchpad como um modificador para apps de DJ, a captura de mouse para jogos e mais atalhos de teclado para usuários focados nesse dispositivo.

Teclado

A maneira como seu app responde à entrada do teclado contribui para a experiência do usuário em telas grandes. Existem três tipos de entrada de teclado: navegação, pressionamentos de tecla e atalhos.

A navegação pelo teclado raramente é implementada em apps voltados para o toque, mas os usuários esperam isso quando estão usando um app e estão com as mãos no teclado. A navegação por teclado pode ser essencial em smartphones, tablets, dispositivos dobráveis e computadores para usuários com necessidades de acessibilidade.

Para muitos apps, a navegação com as teclas de seta e Tab é processada automaticamente pelo framework do Android. Por exemplo, alguns elementos combináveis são focalizáveis por padrão, como um Button ou um elemento combinável com o modificador clickable. A navegação pelo teclado geralmente funciona sem nenhum código adicional. Para ativar a navegação pelo teclado para elementos combináveis personalizados que não são focalizáveis por padrão, adicione o modificador focusable:

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

Para mais informações, consulte Como tornar um elemento combinável focado.

Quando o foco é ativado, o framework do Android cria um mapeamento de navegação para todos os componentes focalizáveis com base na posição deles. Isso geralmente funciona conforme o esperado, e não é necessário realizar qualquer outro desenvolvimento.

No entanto, o Compose nem sempre determina o próximo item correto para a navegação com guias para elementos combináveis complexos, como guias e listas, por exemplo, quando um dos elementos combináveis é um rolável horizontal que não está totalmente visível.

Para controlar o comportamento de foco, adicione o modificador focusGroup ao elemento combinável pai de uma coleção de elementos combináveis. O foco se move para o grupo e, em seguida, para o grupo antes de passar para o próximo componente em foco, por exemplo:

Row {
    Column(Modifier.focusGroup()) {
        Button({}) { Text("Row1 Col1") }
        Button({}) { Text("Row2 Col1") }
        Button({}) { Text("Row3 Col1") }
    }
    Column(Modifier.focusGroup()) {
        Button({}) { Text("Row1 Col2") }
        Button({}) { Text("Row2 Col2") }
        Button({}) { Text("Row3 Col2") }
    }
}

Para mais informações, consulte Oferecer navegação coerente com grupos de foco.

Teste o acesso a todos os elementos da interface do app usando apenas o teclado. Os elementos usados com frequência precisam ser acessíveis sem o mouse ou a entrada de toque.

A compatibilidade com o teclado pode ser essencial para usuários com necessidades de acessibilidade.

Pressionamentos de tecla

Para entrada de texto que seria processada por um teclado virtual na tela (IME), como para um TextField , os apps precisam se comportar como esperado em dispositivos de tela grande sem trabalho extra de desenvolvimento. Para pressionamentos de tecla que não podem ser previstos pelo framework, os apps precisam processar o comportamento sozinhos. Isso ocorre especialmente em apps com visualizações personalizadas.

Alguns exemplos são apps de chat que usam a tecla Enter para enviar uma mensagem, apps de mídia que iniciam e interrompem a reprodução com a barra de espaço e jogos que controlam o movimento com as teclas w, a, s e d.

É possível processar pressionamentos de tecla individuais com o modificador onKeyEvent, que aceita uma lambda chamada quando o componente modificado recebe um evento de tecla. A propriedade KeyEvent#type permite determinar se o evento é uma pressão de tecla (KeyDown) ou uma liberação de tecla (KeyUp):

Box(
    modifier = Modifier.focusable().onKeyEvent {
        if(
            it.type == KeyEventType.KeyUp &&
            it.key == Key.S
        ) {
            doSomething()
            true
        } else {
            false
        }
    }
)  {
    Text("Press S key")
}

Como alternativa, é possível substituir o callback onKeyUp() e adicionar o comportamento esperado para cada código de tecla recebido:

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
    return when (keyCode) {
        KeyEvent.KEYCODE_ENTER -> {
            sendChatMessage()
            true
        }
        KeyEvent.KEYCODE_SPACE -> {
            playOrPauseMedia()
            true
        }
        else -> super.onKeyUp(keyCode, event)
    }
}

Um evento onKeyUp ocorre quando uma tecla é liberada. O uso do callback impede que os apps precisem processar vários eventos onKeyDown se uma tecla for mantida pressionada ou liberada lentamente. Jogos e apps que precisam detectar o momento em que uma tecla é pressionada ou se o usuário está pressionando uma tecla podem detectar o evento onKeyDown e processar eventos onKeyDown repetidos.

Para mais informações, consulte Processar ações do teclado.

Atalhos

Os atalhos de teclado comuns que incluem as teclas Ctrl, Alt, Shift e Meta são esperados ao usar um teclado físico. Se um app não implementar atalhos, a experiência dos usuários pode ser frustrante. Além disso, atalhos para tarefas específicas usadas com frequência agradam usuários avançados. Os atalhos facilitam o uso e diferenciam o app de apps que não têm atalhos.

Alguns atalhos comuns incluem Ctrl+S (salvar), Ctrl+Z (desfazer) e Ctrl+Shift+Z (refazer). Para conferir uma lista de atalhos padrão, consulte Processar ações do teclado.

Um objeto KeyEvent tem os seguintes atributos, que indicam se as teclas modificadoras estão pressionadas:

Exemplo:

Box(
    Modifier.onKeyEvent {
        if (it.isAltPressed && it.key == Key.A) {
            println("Alt + A is pressed")
            true
        } else {
            false
        }
    }
    .focusable()
)

Para saber mais, consulte os tópicos abaixo:

Stylus

Muitos dispositivos de tela grande vêm equipados com uma stylus. Os apps Android processam stylus como entrada de tela touchscreen. Alguns dispositivos também podem ter uma prancheta de desenho USB ou Bluetooth, como o Wacom Intuos (link em inglês). Os apps Android podem receber entrada por Bluetooth, mas não por USB.

Para acessar objetos de stylus MotionEvent, adicione o modificador pointerInteropFilter a uma superfície de desenho. Implemente uma classe ViewModel com um método que processa eventos de movimento. Transmita o método como a lambda onTouchEvent do modificador pointerInteropFilter:

@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun DrawArea(modifier: Modifier = Modifier) {
   Canvas(modifier = modifier
       .clipToBounds()
       .pointerInteropFilter {
           viewModel.processMotionEvent(it)
       }

   ) {
       // Drawing code here.
   }
}

O objeto MotionEvent contém informações sobre o evento:

Pontos históricos

O Android agrupa eventos de entrada e os envia uma vez por frame. Uma stylus pode relatar eventos em frequências muito maiores do que a tela. Ao criar apps de desenho, verifique eventos que podem estar no passado recente usando as APIs getHistorical:

Rejeição da palma da mão

Quando os usuários desenham, escrevem ou interagem com o app usando uma stylus, às vezes, eles tocam na tela com a palma da mão. O evento de toque (definido como ACTION_DOWN ou ACTION_POINTER_DOWN) pode ser informado ao app antes que o sistema reconheça e desconsidere o toque acidental da palma da mão.

O Android cancela eventos de toque da palma da mão enviando um MotionEvent. Se o app receber ACTION_CANCEL, cancele o gesto. Se o app receber ACTION_POINTER_UP, verifique se FLAG_CANCELED está definido. Nesse caso, cancele o gesto.

Não verifique apenas o FLAG_CANCELED. No Android 13 (nível 33 da API) e versões mais recentes, o sistema define FLAG_CANCELED para eventos ACTION_CANCEL, mas não define a flag em versões mais antigas do Android.

Android 12

No Android 12 (nível 32 da API) e versões anteriores, a detecção da rejeição da palma da mão é possível apenas para eventos de toque de ponteiro único. Se um toque da palma for o único ponteiro, o sistema vai cancelar o evento definindo ACTION_CANCEL no objeto do evento de movimento. Se outros ponteiros estiverem inativos, o sistema vai definir ACTION_POINTER_UP, o que é insuficiente para detectar a rejeição da palma da mão.

Android 13

No Android 13 (API de nível 33) e versões mais recentes, se um toque de palma da mão for o único ponteiro, o sistema vai cancelar o evento definindo ACTION_CANCEL e FLAG_CANCELED no objeto do evento de movimento. Se outros ponteiros estiverem inativos, o sistema vai definir ACTION_POINTER_UP e FLAG_CANCELED.

Sempre que o app receber um evento de movimento com ACTION_POINTER_UP, procure o FLAG_CANCELED para determinar se ele indica uma rejeição de palma da mão (ou outro cancelamento de eventos).

Apps de anotações

O ChromeOS tem uma intent especial que mostra apps de anotações registrados para os usuários. Para registrar um app como de anotações, adicione o seguinte ao manifesto do app:

<intent-filter>
    <action android:name="org.chromium.arc.intent.action.CREATE_NOTE" />
    <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

Quando um app é registrado no sistema, o usuário pode selecioná-lo como o app de anotações padrão. Quando uma nova anotação é solicitada, o app precisa criar uma nota vazia pronta para entrada da stylus. Quando o usuário quiser escrever em uma imagem (como uma captura de tela ou imagem salva), o app será iniciado com ClipData contendo um ou mais itens com URIs content://. O app precisa criar uma nota que use a primeira imagem anexada como uma imagem de plano de fundo e entrar em um modo em que o usuário possa desenhar na tela com uma stylus.

Testar intents de anotações sem uma stylus

[TBD remove section.]

Para testar se um app responde corretamente a intents de anotação sem uma stylus ativa, use o seguinte método para mostrar as opções de anotação no ChromeOS:

  1. Alterne para o modo de desenvolvedor e torne o dispositivo gravável.
  2. Pressione Ctrl + Alt + F2 para abrir um terminal.
  3. Execute o comando sudo vi /etc/chrome_dev.conf.
  4. Pressione i para editar e adicionar --ash-enable-palette a uma nova linha no final do arquivo.
  5. Salve pressionando Esc e digitando :, w, q e pressionando Enter.
  6. Pressione Ctrl + Alt + F1 para retornar à interface normal do ChromeOS.
  7. Saia e faça login novamente

Agora você vai ver um menu da stylus na estante:

  • Toque no botão da stylus na estante e escolha Nova nota. Isso vai abrir uma nota de desenho em branco.
  • Faça uma captura de tela. Na estante, selecione botão da stylus > Capturar tela ou faça o download de uma imagem. Você vai encontrar a opção Anotar imagem na notificação. O app vai iniciar com a imagem pronta para ser anotada.

Compatibilidade com mouse e touchpad

Geralmente, a maioria dos apps precisa processar apenas três eventos grandes com foco na tela: clicar com o botão direito do mouse, passar o cursor e arrastar e soltar.

Clicar com o botão direito

Todas as ações que fazem com que um app mostre um menu de contexto, por exemplo, tocar em um item de lista e pressionar, também precisam reagir a eventos de clique com o botão direito do mouse.

Para processar eventos de clique com o botão direito do mouse, os apps precisam registrar um View.OnContextClickListener:

Box(modifier = Modifier.fillMaxSize()) {
    AndroidView(
        modifier = Modifier.fillMaxSize(),
        factory = { context ->
            val rootView = FrameLayout(context)
            val onContextClickListener =
                View.OnContextClickListener { view ->
                    showContextMenu()
                    true
                }
            rootView.setOnContextClickListener(onContextClickListener)
            rootView
        },
    )
}

Para saber detalhes sobre como criar menus de contexto, consulte Criar um menu contextual.

Passar cursor

Você pode fazer com que os layouts do app pareçam bem-acabados e mais fáceis de usar processando eventos de passar o cursor. Isso é especialmente verdadeiro para componentes personalizados:

Os dois exemplos mais comuns são:

  • Indicar aos usuários se um elemento tem comportamento interativo, como ser clicável ou editável, mudando o ícone do ponteiro do mouse
  • adicionar feedback visual a itens em uma grade ou lista grande quando o ponteiro do mouse estiver sobre eles.

Arrastar e soltar

Em um ambiente de várias janelas, os usuários esperam poder arrastar e soltar itens entre apps. Isso é válido para dispositivos desktop, tablets, smartphones e dispositivos dobráveis no modo de tela dividida.

Considere se os usuários vão arrastar itens para o app. Por exemplo, editores de fotos provavelmente recebem fotos, players de áudio recebem arquivos de áudio e programas de desenho recebem imagens.

Para adicionar suporte ao recurso de arrastar e soltar, consulte Arrastar e soltar e confira a postagem do blog Android no ChromeOS: como implementar o recurso de arrastar e soltar.

Considerações especiais para o ChromeOS

Compatibilidade avançada com ponteiro

Os apps que processam entradas avançadas de mouse e touchpad precisam implementar um modificador pointerInput para receber um PointerEvent:

@Composable
private fun LogPointerEvents(filter: PointerEventType? = null) {
    var log by remember { mutableStateOf("") }
    Column {
        Text(log)
        Box(
            Modifier
                .size(100.dp)
                .background(Color.Red)
                .pointerInput(filter) {
                    awaitPointerEventScope {
                        while (true) {
                            val event = awaitPointerEvent()
                            // handle pointer event
                            if (filter == null || event.type == filter) {
                                log = "${event.type}, ${event.changes.first().position}"
                            }
                        }
                    }
                }
        )
    }
}

Examine o objeto PointerEvent para determinar o seguinte:

Controles de jogos

Alguns dispositivos Android de tela grande oferecem suporte para até quatro controles de jogo. Use as APIs padrão de controle de jogos do Android para processar controles de jogos. Consulte Suporte a controles de jogos.

Os botões do controle do jogo são mapeados para valores comuns após um mapeamento comum. Mas nem todos os fabricantes de controles de jogos seguem as mesmas convenções de mapeamento. Você pode fornecer uma experiência muito melhor aos usuários se permitir que eles selecionem entre os mapeamentos de controles mais usados. Consulte Processar o pressionamento do botão do gamepad para mais informações.

Modo de tradução de entrada

O ChromeOS ativa um modo de tradução de entrada por padrão. Para a maioria dos apps Android, esse modo os ajuda a funcionar como esperado em um ambiente de computador. Alguns exemplos incluem ativar automaticamente a rolagem com dois dedos no touchpad, a rolagem da roda do mouse e o mapeamento de coordenadas de exibição brutas para coordenadas de janela. Geralmente, os desenvolvedores de apps não precisam implementar nenhum desses comportamentos.

Se um app implementar um comportamento de entrada personalizado, como, por exemplo, definir uma ação personalizada de gesto de pinça com dois dedos ou se essas traduções de entrada não fornecerem os eventos de entrada esperados pelo app, você pode desativar o modo de tradução de entrada adicionando esta tag ao manifesto do Android:

<uses-feature
    android:name="android.hardware.type.pc"
    android:required="false" />

Outros recursos