Como processar mudanças de entrada

Os Chromebooks oferecem aos usuários várias opções de entrada: teclado, mouse, trackpads, touchscreens, stylus, MIDI e gamepad/controles Bluetooth. Isso significa que o mesmo dispositivo pode se tornar a estação de um DJ, a tela de um artista ou a plataforma preferida de um gamer para jogos AAA por streaming.

Como desenvolvedor, isso oferece a oportunidade de criar experiências de apps versáteis e interessantes para seus usuários, aproveitando os dispositivos de entrada que eles já têm em mãos, desde um teclado conectado até uma stylus ou um controle de jogo do Stadia. No entanto, todas essas possibilidades também exigem que você pense na interface para tornar a experiência do app tranquila e lógica. Isso é ainda mais verdadeiro se o app ou jogo foi projetado para smartphones. Por exemplo, se o jogo tiver um joystick sensível ao toque na tela para smartphones, é recomendável ocultá-lo quando um usuário estiver jogando com o teclado.

Nesta página, você vai encontrar os principais problemas a serem considerados ao pensar em várias fontes de entrada e estratégias para resolvê-los.

Descoberta de métodos de entrada compatíveis pelo usuário

O ideal é que o app responda sem problemas a qualquer entrada que o usuário escolher. Muitas vezes, isso é simples e não exige que você forneça informações extras ao usuário. Por exemplo, um botão precisa funcionar se o usuário clicar nele com um mouse, um trackpad, a tela touchscreen, uma stylus etc. e não é necessário informar ao usuário que ele pode usar esses diferentes dispositivos para ativar o botão.

No entanto, há situações em que o método de entrada pode melhorar a experiência do usuário, e pode ser interessante informar que o app é compatível com ele. Alguns exemplos:

  • Um app de reprodução de mídia pode oferecer muitos atalhos de teclado úteis que o usuário não consegue adivinhar com facilidade.
  • Se você criou um app de DJ, o usuário pode usar a tela sensível ao toque no início e não perceber que você permitiu o uso do teclado/trackpad para fornecer acesso tátil a alguns dos recursos. Da mesma forma, eles podem não saber que você oferece suporte a vários controladores de DJ MIDI. Incentivar as pessoas a conferir os hardwares compatíveis pode proporcionar uma experiência de DJ mais autêntica.
  • Seu jogo pode ser ótimo com touchscreen e teclado/mouse, mas os usuários talvez não saibam que ele também é compatível com vários controles de jogos Bluetooth. Conectar um deles pode aumentar a satisfação e o engajamento do usuário.

Você pode ajudar os usuários a descobrir opções de entrada com mensagens no momento adequado. A implementação será diferente para cada app. Alguns exemplos incluem:

  • Pop-ups de primeira execução ou dicas do dia
  • As opções de configuração nos painéis podem indicar passivamente aos usuários que há suporte. Por exemplo, uma guia "Controle de jogo" no painel de configurações de um jogo indica que controles são compatíveis.
  • Mensagens contextuais. Por exemplo, se você detectar um teclado físico e descobrir que o usuário está clicando em uma ação usando um mouse ou uma tela sensível ao toque, talvez queira mostrar uma dica útil sugerindo um atalho de teclado.
  • Listas de atalhos do teclado. Quando um teclado físico é detectado, mostrar na interface uma maneira de abrir uma lista de atalhos do teclado disponíveis tem o objetivo duplo de anunciar que há suporte para teclado e oferecer uma maneira fácil para os usuários verem e lembrarem os atalhos compatíveis.

Layout/rotulagem da interface para variação de entrada

O ideal é que a interface visual não precise mudar muito se um dispositivo de entrada diferente for usado. Todas as entradas possíveis devem "simplesmente funcionar". No entanto, há exceções importantes. Dois dos principais são a interface específica para toque e os comandos na tela.

Interface específica para toque

Sempre que o app tiver elementos de interface específicos para toque, como um joystick na tela em um jogo, considere como será a experiência do usuário quando o toque não estiver sendo usado. Em alguns jogos para dispositivos móveis, uma parte significativa da tela é usada pelos controles necessários para jogar com toque, mas desnecessários se o usuário estiver usando um gamepad ou teclado. O app ou jogo precisa fornecer lógica para detectar qual método de entrada está sendo usado e ajustar a interface de acordo. Consulte a seção de implementação abaixo para ver alguns exemplos de como fazer isso.

Interfaces de jogos de corrida de carros: uma com controles na tela e outra com teclado

Comandos na tela

Seu app pode estar oferecendo comandos úteis na tela para os usuários. Tome cuidado para que eles não dependam de dispositivos de entrada. Exemplo:

  • Deslize para…
  • Toque em qualquer lugar para fechar
  • Fazer gesto de pinça para aplicar zoom
  • Pressione "X" para…
  • Toque e mantenha pressionado para ativar

Alguns apps podem ajustar a linguagem para serem independentes da entrada. Isso é preferível quando faz sentido, mas em muitos casos a especificidade é importante, e talvez seja necessário mostrar mensagens diferentes dependendo do método de entrada usado, principalmente em modos de tutorial, como na primeira execução dos apps.

Considerações sobre vários idiomas

Se o app for compatível com vários idiomas, pense na arquitetura de strings. Por exemplo, se você oferece suporte a três modos de entrada e cinco idiomas, isso pode significar 15 versões diferentes de cada mensagem da interface. Isso vai aumentar a quantidade de trabalho necessária para adicionar novos recursos e ampliar a probabilidade de erros de ortografia.

Uma abordagem é pensar nas ações da interface como um conjunto separado de strings. Por exemplo, se você definir a ação "fechar" como uma variável de string própria com variantes específicas de entrada, como "Toque em qualquer lugar para fechar", "Pressione "Esc" para fechar", "Clique em qualquer lugar para fechar", "Pressione qualquer botão para fechar", todas as mensagens da interface que precisam informar ao usuário como fechar algo podem usar essa única variável de string "fechar". Quando o método de entrada muda, basta alterar o valor dessa variável.

Entrada do teclado de software / IME

Se um usuário estiver usando um app sem um teclado físico, a entrada de texto poderá ocorrer por um teclado na tela. Teste se os elementos de interface do usuário necessários não ficam ocultos quando um teclado na tela aparece. Consulte a documentação sobre visibilidade do IME do Android para mais informações.

Implementação

Na maioria dos casos, os apps ou jogos precisam responder corretamente a todas as entradas válidas, independente do que é mostrado na tela. Se um usuário estiver usando a tela touch por 10 minutos, mas de repente mudar para o teclado, a entrada de texto precisa funcionar, independente dos comandos visuais ou controles na tela. Em outras palavras, a funcionalidade deve ter prioridade sobre elementos visuais/texto.Isso ajuda a garantir que seu app/jogo seja utilizável mesmo que a lógica de detecção de entrada tenha um erro ou surja uma situação inesperada.

A próxima etapa é mostrar a interface correta para o método de entrada que está sendo usado. Como você detecta isso com precisão? O que acontece se os usuários alternarem entre diferentes métodos de entrada enquanto usam seu app? E se eles estiverem usando vários métodos ao mesmo tempo?

Máquina de estado priorizada com base em eventos recebidos

Uma abordagem é acompanhar o "estado de entrada ativo" atual, que representa os comandos de entrada mostrados na tela para o usuário. Para isso, acompanhe os eventos de entrada reais recebidos pelo app e faça a transição entre os estados usando uma lógica priorizada.

Priorizar

Por que priorizar estados de entrada? Os usuários interagem com os dispositivos de várias maneiras, e seu app precisa oferecer suporte a essas escolhas. Por exemplo, se um usuário optar por usar a tela sensível ao toque e um mouse Bluetooth ao mesmo tempo, isso precisa ser compatível. Mas quais comandos e controles de entrada na tela você deve mostrar? Mouse ou toque?

Definir cada conjunto de comandos como um "estado de entrada" e priorizá-los pode ajudar a decidir isso de maneira consistente.

Os eventos de entrada recebidos determinam o estado

Por que só agir em eventos de entrada recebidos? Você pode estar pensando em rastrear conexões Bluetooth para indicar se um controlador Bluetooth foi conectado ou monitorar conexões USB para dispositivos USB. Essa abordagem não é recomendada por dois motivos principais.

Primeiro, as informações que você pode deduzir sobre dispositivos conectados com base em variáveis de API não são consistentes, e o número de dispositivos Bluetooth/hardware/stylus está em constante crescimento.

O segundo motivo para usar eventos recebidos em vez do status da conexão é que os usuários podem ter um mouse, um controle Bluetooth, um controle MIDI etc. conectados, mas não estar usando ativamente para interagir com seu app ou jogo.

Ao responder a eventos de entrada que foram recebidos ativamente pelo app, você garante que está respondendo às ações reais dos usuários em tempo real, e não tentando adivinhar as intenções deles com informações incompletas.

Exemplo: jogo com suporte para toque, teclado/mouse e controle

Imagine que você desenvolveu um jogo de corrida de carros para smartphones com tela sensível ao toque. Os jogadores podem acelerar, desacelerar, virar à direita, virar à esquerda ou usar nitro para aumentar a velocidade.

A interface touchscreen atual consiste em um joystick na tela, no canto inferior esquerdo, para os controles de velocidade e direção, e um botão no canto inferior direito para o nitro. O usuário pode coletar recipientes de nitro na pista e, quando a barra de nitro na parte de baixo da tela estiver cheia, uma mensagem vai aparecer acima do botão dizendo "Pressione para nitro!". Se for o primeiro jogo do usuário ou se não houver entrada por um tempo, um texto de "tutorial" vai aparecer acima do joystick mostrando como fazer o carro se mover.

Você quer adicionar suporte para teclado e controle de jogo Bluetooth. Por onde começar?

Jogo de corrida de carros com controles de toque

Estados de entrada

Comece identificando todos os estados de entrada em que seu jogo pode estar sendo executado e liste todos os parâmetros que você quer mudar em cada estado.

                                                                                                                                                                        
TocarTeclado/mouseControlador de jogos
       

Reações a

     
       

Todas as entradas

     
       

Todas as entradas

     
       

Todas as entradas

     
       

Controles na tela

     
       

- Joystick na tela
- Botão Nitro

     
       

- Sem joystick
- Sem botão de nitro

     
       

- Sem joystick
- Sem botão de nitro

     
       

Texto

     
       

Toque para Nitro!

     
       

Pressione "N" para Nitro!

     
       

Pressione "A" para Nitro!

     
       

Tutorial

     
       

Imagem do joystick para velocidade/direção

     
       

Imagem das teclas de seta e WASD para velocidade/direção

     
       

Imagem do gamepad para velocidade/direção

     

Acompanhe o estado de "entrada ativa" e atualize a interface conforme necessário com base nesse estado.

Observação: seu jogo/app precisa responder a todos os métodos de entrada, independente do estado. Por exemplo, se um usuário estiver dirigindo o carro com a tela touchscreen, mas pressionar N no teclado, a ação de nitro deverá ser acionada.

Mudanças de estado priorizadas

Alguns usuários podem usar a tela touchscreen e o teclado ao mesmo tempo. Alguns podem começar a usar seu jogo/app no sofá no modo tablet e depois mudar para o teclado na mesa. Outros podem conectar ou desconectar controles de jogos no meio da partida.

O ideal é não ter elementos de interface incorretos, como dizer ao usuário para pressionar a tecla "n" quando ele está usando a tela sensível ao toque. Ao mesmo tempo, no caso de usuários que usam vários dispositivos de entrada simultaneamente, como tela sensível ao toque e teclado, não é recomendável que a interface troque constantemente entre os dois estados.

Uma maneira de lidar com isso é estabelecer prioridades de tipo de entrada e criar um atraso entre as mudanças de estado. No jogo de carro acima, você sempre vai querer garantir que o joystick na tela esteja visível sempre que eventos de toque na tela forem recebidos. Caso contrário, o jogo pode parecer inutilizável para o usuário. Isso faria com que a tela sensível ao toque fosse o dispositivo de maior prioridade.

Se eventos de teclado e touchscreen forem recebidos simultaneamente, o jogo vai permanecer no modo touchscreen, mas ainda vai reagir à entrada do teclado. Se nenhuma entrada de tela touchscreen for recebida após 5 segundos e os eventos de teclado ainda estiverem sendo recebidos, talvez os controles na tela desapareçam e o jogo passe para o estado do teclado.

A entrada do controle de jogo seguiria um padrão semelhante: o estado da interface do usuário do controle teria uma prioridade menor do que o teclado/mouse e o toque e só apareceria se a entrada do controle de jogo, e não outras formas de entrada, estivesse sendo recebida. O jogo sempre responderia à entrada do controle.

Confira abaixo um diagrama de estado e uma tabela de transição para o exemplo. Adapte a ideia ao seu app ou jogo.

Máquina de estado priorizada: tela touch, teclado/mouse, controle de jogo

                                                                                                                                        
#1 Tela touch2. Teclado#3 Gamepad
       

Mover para #1

     
       

N/A

     
       

- Entrada por toque recebida
- Mover imediatamente para o estado de entrada por toque

     
       

- Entrada por toque recebida
- Mover imediatamente para o estado de entrada por toque

     
       

Mover para #2

     
       

- Sem toque por 5 segundos
- Entrada do teclado recebida
- Mover para o estado de entrada do teclado

     
       

N/A

     
       

- Entrada do teclado recebida
(mova imediatamente para o estado de entrada do teclado)

     
       

Mover para #3

     
       

- Sem toque por 5s
- Sem teclado por 5s
- Entrada do gamepad recebida
- Mover para o estado de entrada do gamepad

     
       

- Nenhum teclado por 5s
- Entrada do gamepad recebida
- Mover para o estado de entrada do gamepad

     
       

N/A

     

Observação:a priorização ajuda a deixar claro qual tipo de entrada deve ser dominante. O estado de entrada instantaneamente passa a ter maior prioridade:

3. Gamepad -> 2. Teclado -> 1. Tocar

assim que um dispositivo de prioridade mais alta for usado, mas ele lentamente passará para uma prioridade "menor", somente após um período de atraso e se o dispositivo de prioridade mais baixa estiver sendo usado ativamente.

Eventos de entrada

Confira um exemplo de código para detectar eventos de entrada de vários tipos de dispositivos usando as APIs padrão do Android. Use esses eventos para acionar sua máquina de estado, como acima. Adapte o conceito geral aos tipos de eventos de entrada esperados e ao seu app ou jogo.

Botões do teclado e do controle

// Drive the state machine based on the received input type
// onKeyDown drives the state machine, but does not trigger game actions
// Both keyboard and game controller events come through as key events
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    if (event != null) {
        // Check input source
        val outputMessage = when (event.source) {
            SOURCE_KEYBOARD -> {
                MyStateMachine.KeyboardEventReceived()
                "Keyboard event"
            }
            SOURCE_GAMEPAD -> {
                MyStateMachine.ControllerEventReceived()
                "Game controller event"
            }
            else -> "Other key event: ${event.source}"
        }
        // Do something based on source type
        findViewById(R.id.text_message).text = outputMessage
    }
    // Pass event up to system
    return super.onKeyDown(keyCode, event)
}
// Trigger game events based on key release
// Both keyboard and game controller events come through as key events
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
   when(keyCode) {
       KeyEvent.KEYCODE_N -> {
           MyStateMachine.keyboardEventReceived()
           engageNitro()
           return true // event handled here, return true
       }
   }
   // If event not handled, pass up to system
   return super.onKeyUp(keyCode, event)
}

Observação:para KeyEvents, você pode usar onKeyDown() ou onKeyUp(). Aqui, onKeyDown() é usado para controlar a máquina de estado, enquanto onKeyUp() é usado para acionar eventos do jogo.

Se um usuário tocar e manter um botão pressionado, onKeyUp() será acionado apenas uma vez por pressionamento de tecla, enquanto onKeyDown() será chamado várias vezes. Se você quiser reagir ao pressionamento para baixo, processe os eventos do jogo em onKeyDown() e implemente uma lógica para lidar com os eventos repetidos. Consulte a documentação Processar ações do teclado para mais informações.

Toque e stylus

// Touch and stylus events come through as touch events
override fun onTouchEvent(event: MotionEvent?): Boolean {
   if (event != null) {
       // Get tool type
       val pointerIndex = event.action and ACTION_POINTER_INDEX_MASK shr ACTION_POINTER_INDEX_SHIFT
       val toolType = event.getToolType(pointerIndex)

       // Check tool type
       val outputMessage = when (toolType) {
           TOOL_TYPE_FINGER -> {
               MyStateMachine.TouchEventReceived()
               "Touch event"
           }
           TOOL_TYPE_STYLUS -> {
                MyStateMachine.StylusEventReceived()
               "Stylus event"
           }
           else -> "Other touch event: ${toolType}"
       }

       // Do something based on tool type, return true if event handled
       findViewById(R.id.text_message).text = outputMessage
   }
   // If event not handled, pass up to system
   return super.onGenericMotionEvent(event)
}

Mouse e joystick

// Mouse and joystick events come through as generic events
override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
   if (event != null) {
       // Check input source
       val outputMessage = when (event.source) {
           SOURCE_JOYSTICK -> {
                MyStateMachine.ControllerEventReceived()
               "Controller event"
           }
           SOURCE_MOUSE -> {
                MyStateMachine.MouseEventReceived()
               "Mouse event"
           }
           else -> "Other generic event: ${event.source}"
       }
       // Do something based on source type, return true if event handled
       findViewById(R.id.text_message).text = outputMessage
   }
   // If event not handled, pass up to system
   return super.onGenericMotionEvent(event)
}