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.
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?
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.
| Tocar | Teclado/mouse | Controlador de jogos | |
|---|---|---|---|
|
Reações a |
Todas as entradas |
Todas as entradas |
Todas as entradas |
|
Controles na tela |
- Joystick na tela |
- Sem joystick |
- Sem joystick |
|
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.
| #1 Tela touch | 2. Teclado | #3 Gamepad | |
|---|---|---|---|
|
Mover para #1 |
N/A |
- Entrada por toque recebida |
- Entrada por toque recebida |
|
Mover para #2 |
- Sem toque por 5 segundos |
N/A |
- Entrada do teclado recebida |
|
Mover para #3 |
- Sem toque por 5s |
- Nenhum teclado por 5s |
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) }