1. Introdução
Os usuários podem interagir com seu app usando um teclado físico, normalmente em dispositivos de tela grande, como tablets e dispositivos ChromeOS, mas também em dispositivos XR. É importante que os usuários possam navegar no app com um teclado físico ou uma tela touch. Além disso, ao projetar seu app para TVs e telas de carro, que podem não ter entrada por toque e depender de botões direcionais ou codificadores rotativos, é necessário aplicar princípios semelhantes à navegação por teclado.
O Compose permite processar entradas de teclados físicos, botões direcionais e codificadores rotativos de maneira unificada. Um princípio importante de uma boa experiência do usuário para esses métodos de entrada é permitir que os usuários possam mover o foco do teclado de forma intuitiva e consistente para o componente interativo com que querem interagir.
Neste codelab, você vai aprender:
- Como implementar padrões comuns de gerenciamento de foco do teclado para uma navegação intuitiva e consistente
- Como testar se o movimento do foco do teclado se comporta como esperado
Pré-requisitos
- Experiência na criação de apps com o Compose
- Conhecimento básico de Kotlin, incluindo lambdas e corrotinas
O que você vai criar
Você vai implementar estes padrões típicos de gerenciamento de foco do teclado:
- Movimento do foco do teclado: do início ao fim, de cima para baixo no padrão em forma de Z
- Foco inicial lógico: defina o foco como o elemento da interface com que o usuário provavelmente vai interagir
- Restauração do foco: mova o foco para o elemento da interface com que o usuário interagiu anteriormente
O que você aprenderá
- Noções básicas de gerenciamento de foco no Compose
- Como definir um elemento da interface como alvo de foco
- Como solicitar o foco para mover um elemento da interface
- Como mover o foco do teclado para um determinado elemento da interface em um grupo desses elementos
O que você precisa
- Android Studio Ladybug ou versão mais recente
- Qualquer um destes dispositivos para executar o app de exemplo:
- Um dispositivo de tela grande com um teclado físico
- Um dispositivo virtual Android para dispositivos de tela grande, como o emulador redimensionável
2. Configurar
- Clone o repositório do GitHub para codelabs de tela grande:
git clone https://github.com/android/large-screen-codelabs
Como alternativa, você pode baixar e extrair o arquivo ZIP para codelabs de tela grande:
- Navegue até a pasta
focus-management-in-compose
. - No Android Studio, abra o projeto. A pasta
focus-management-in-compose
contém um projeto. - Se você não tiver um tablet Android, um dispositivo dobrável ou um dispositivo ChromeOS com teclado físico, abra o Gerenciador de dispositivos no Android Studio e crie o dispositivo Resizable na categoria Phone.
Figura 1. Como configurar o emulador redimensionável no Android Studio.
3. Explorar o código inicial
O projeto tem dois módulos:
- start: contém o código inicial do projeto. Você vai fazer edições nesse código para concluir o codelab.
- solution: contém o código concluído deste codelab.
O app de exemplo tem três guias:
- Focus target (alvo de foco)
- Focus traversal order (ordem de apresentação do foco)
- Focus group (grupo de foco)
A guia "Focus target" aparece quando o app é iniciado.
Figura 2. A guia Focus target aparece quando o app é iniciado.
O pacote ui
contém o código de UI abaixo com que você interage:
App.kt
: implementa guiastab.FocusTargetTab.kt
: contém o código da guia "Focus target"tab.FocusTraversalOrderTab.kt
: contém o código da guia "Focus traversal order"tab.FocusGroup.kt
: contém o código da guia "Focus group"FocusGroupTabTest.kt
: um teste instrumentado paratab.FocusTargetTab.kt
. O arquivo está localizado na pastaandroidTest
.
4. Alvo de foco
Um alvo de foco é um elemento da interface para que o foco do teclado pode se mover. Os usuários podem mover o foco do teclado com a tecla Tab
ou as teclas direcionais (setas):
- Tecla
Tab
: o foco se move para o próximo alvo de foco ou para o alvo anterior de forma unidimensional. - Teclas direcionais: o foco pode se mover em duas dimensões, para cima ou para baixo e para a esquerda ou para a direita.
As guias são alvos de foco. No app de exemplo, o plano de fundo das guias é atualizado visualmente quando a guia recebe o foco.
Figura 3. O plano de fundo do componente muda quando o foco muda para um alvo de foco.
Os elementos interativos da interface são alvos de foco por padrão
Um componente interativo é um alvo de foco por padrão. Em outras palavras, o elemento da interface é um alvo de foco se os usuários podem tocar nele.
O app de exemplo tem três cards na guia Focus target. O primeiro card e o terceiro card são alvos de foco. O segundo card não é. O plano de fundo do terceiro card é atualizado quando o usuário move o foco do primeiro com a tecla Tab
.
Figura 4. Os alvos de foco do app excluem o segundo card.
Modificar o segundo card para que seja um alvo de foco
Você pode tornar o segundo card um alvo de foco tornando-o um elemento de interface interativo. A maneira mais fácil é usar o modificador clickable
desta forma:
- Abra
FocusTargetTab.kt
no pacotetabs
. - Modifique o elemento combinável
SecondCard
com o modificadorclickable
desta forma:
@Composable
fun FocusTargetTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
SecondCard(
modifier = Modifier
.width(240.dp)
.clickable(onClick = onClick)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
}
Executar
Agora, o usuário pode mover o foco para o segundo card, além do primeiro e do terceiro. Você pode testar isso na guia Focus target. Confirme se é possível mover o foco do primeiro card para o segundo usando a tecla Tab
.
Figura 5. Mover o foco do primeiro card para o segundo com a tecla Tab
.
5. Transição de foco em um padrão em Z
Os usuários esperam que o foco do teclado se mova da esquerda para a direita e de cima para baixo quando o padrão de leitura do idioma configurado é da esquerda para a direita. Essa ordem de apresentação de foco é chamada de padrão em Z.
No entanto, o Compose ignora o layout quando determina o próximo alvo de foco da tecla Tab
e usa a travessia de foco unidimensional com base na ordem das chamadas de funções combináveis.
Transição de foco unidimensional
A ordem de apresentação de foco unidimensional vem da ordem das chamadas de funções combináveis, e não do layout do app.
No app de exemplo, o foco se move nesta ordem na guia Focus traversal order:
- Primeiro card
- Quarto card
- Terceiro card
- Segundo card
Figura 6. A travessia de foco segue a ordem das funções combináveis.
A função FocusTraversalOrderTab
implementa a guia Focus traversal do app de exemplo. A função chama funções combináveis para os cards: FirstCard
, FourthCard
, ThirdCard
e SecondCard
, nessa ordem.
@Composable
fun FocusTraversalOrderTab(
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.offset(x = 256.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.offset(y = (-151).dp)
)
}
SecondCard(
modifier = Modifier.width(240.dp)
)
}
}
Movimento do foco no padrão em Z
É possível integrar o movimento do foco no padrão em Z na guia Focus traversal order do app de exemplo seguindo estas etapas:
- Abrir
tabs.FocusTraversalOrderTab.kt
- Remova o modificador de deslocamento dos elementos combináveis
ThirdCard
eFourthCard
. - Mude o layout da guia para uma coluna com duas linhas da linha atual com duas colunas.
- Mova os elementos combináveis
FirstCard
eSecondCard
para a primeira linha. - Mova os elementos combináveis
ThirdCard
eFourthCard
para a segunda linha.
O código modificado ficará assim:
@Composable
fun FocusTraversalOrderTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp),
)
SecondCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
}
}
Executar
Agora, o usuário pode mover o foco da direita para a esquerda, de cima para baixo no padrão em Z. Você pode testar isso na guia Focus traversal order e confirmar que o foco se move na seguinte ordem com a tecla Tab
:
- Primeiro card
- Segundo card
- Terceiro card
- Quarto card
Figura 7. Foco de navegação em um padrão em Z.
6. focusGroup
O foco muda para o terceiro card do primeiro com a tecla direcional right
na guia Focus group. O movimento provavelmente é um pouco confuso para os usuários, já que os dois cards não estão lado a lado.
Figura 8. Movimento inesperado do foco do primeiro card para o terceiro card.
A travessia de foco bidimensional se refere a informações de layout
Pressionar uma tecla de direção aciona a travessia de foco bidimensional. Esse é um foco comum em TVs, já que os usuários interagem com seu app usando um botão direcional. Pressionar as teclas de seta do teclado também aciona a travessia de foco bidimensional, já que elas imitam a navegação com um botão direcional.
Na travessia de foco bidimensional, o sistema se refere às informações geométricas dos elementos da interface e determina o alvo de foco para mover o foco. Por exemplo, o foco é movido para o primeiro card da guia "Focus target" com a tecla direcional down
. Ao pressionar a tecla direcional para cima, o foco é movido para a guia "Focus target".
Figura 9. Mover o foco com as teclas direcionais para cima e para baixo.
A travessia de foco bidimensional não é concatenada, ao contrário da travessia de foco unidimensional com a tecla Tab
. Por exemplo, o usuário não pode mover o foco com a tecla para baixo quando o segundo card recebe o foco.
Figura 10. A tecla de direção para baixo não pode mover o foco quando o segundo card está em foco.
Os alvos de foco estão no mesmo nível
O código abaixo implementa a tela mencionada acima. Há quatro alvos de foco: FirstCard
, SecondCard
, ThirdCard
e FourthCard
. Esses quatro alvos de foco estão no mesmo nível, e ThirdCard
é o primeiro item à direita de FirstCard
no layout. É por isso que o foco muda do primeiro card para o terceiro com a tecla direcional right
.
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
Agrupar alvos de foco com o modificador "focusGroup"
Para mudar o movimento de foco confuso, siga estas etapas:
- Abrir
tabs.FocusGroup.kt
- Modifique a função combinável
Column
na função combinávelFocusGroupTab
com o modificadorfocusGroup
.
O código atualizado ficará assim:
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.focusGroup(),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
O modificador focusGroup
cria um grupo de foco que consiste nos alvos de foco dentro do componente modificado. Os alvos de foco no grupo de foco e os que estão fora dele estão em níveis diferentes, e não há um alvo de foco do lado direito do elemento combinável FirstCard
. Como resultado, o foco não é movido para nenhum card a partir do primeiro com a tecla direcional right
.
Executar
Agora, o foco não muda para o terceiro card do primeiro com a tecla direcional right
na guia "Focus group" do app de exemplo.
7. Solicitar o foco
Os usuários não podem usar teclados ou direcionais para selecionar elementos arbitrários da interface com os quais querem interagir. Os usuários precisam mover o foco do teclado para um componente interativo antes de interagir com o elemento.
Por exemplo, os usuários precisam mover o foco da guia Focus target para o primeiro card antes de interagir com ele. É possível reduzir o número de ações para iniciar a tarefa principal do usuário definindo logicamente o foco inicial.
Figura 11. Pressionar a tecla Tab
três vezes move o foco para o primeiro card.
Solicitar foco com o FocusRequester
É possível solicitar o foco para mover um elemento da interface usando o FocusRequester
. Um objeto FocusRequester
precisa ser associado a um elemento da interface antes de chamar o método requestFocus()
.
Definir o foco inicial como o primeiro card
Para definir o foco inicial como o primeiro card, siga estas etapas:
- Abrir
tabs.FocusTarget.kt
- Declare o valor
firstCard
na função combinávelFocusTargetTab
e inicialize o valor com um objetoFocusRequester
retornado da funçãoremember
. - Modifique a função combinável
FirstCard
com o modificadorfocusRequester
. - Especifique o valor
firstCard
como o argumento do modificadorfocusRequester
. - Chame a função combinável
LaunchedEffect
com o valorUnit
e chame o método requestFocus() sobre o valorfirstCard
na lambda transmitida para a função combinávelLaunchedEffect
.
Um objeto FocusRequester
é criado e associado a um elemento da interface nas segunda e terceira etapas. Na quinta etapa, é solicitado que o foco se mova para o elemento da interface associado quando o elemento combinável FocusdTargetTab
é combinado pela primeira vez.
O código atualizado ficará assim:
@Composable
fun FocusTargetTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val firstCard = remember { FocusRequester() }
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
FirstCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.focusRequester(focusRequester = firstCard)
)
SecondCard(
modifier = Modifier
.width(240.dp)
.clickable(onClick = onClick)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
LaunchedEffect(Unit) {
firstCard.requestFocus()
}
}
Executar
Agora, o foco do teclado se move para o primeiro card na guia Focus target quando ela é selecionada. Para testar, mude de guia. Além disso, o primeiro card é selecionado quando o app é iniciado.
Figura 12. O foco é movido para o primeiro card quando a guia Focus target está selecionada.
8. Mover o foco para a guia selecionada
É possível especificar o alvo de foco quando o foco do teclado está entrando em um grupo de foco. Por exemplo, você pode mover o foco para a guia selecionada quando o usuário estiver movendo o foco para a linha de guias.
É possível implementar esse comportamento seguindo estas etapas:
- Abra
App.kt
. - Declare o valor
focusRequesters
na função combinávelApp
. - Inicialize o valor
focusRequesters
com o valor de retorno da funçãoremember
, que retorna uma lista de objetosFocusRequester
. O comprimento da lista retornada precisa ser igual ao deScreens.entries
. - Associe cada objeto
FocusRequester
do valorfocusRequester
ao elemento combinávelTab
modificando o elemento combinável da guia com o modificadorfocusRequester
. - Modifique o elemento combinável PrimaryTabRow com os modificadores
focusProperties
efocusGroup
. - Transmita uma lambda ao modificador
focusProperties
e associe a propriedadeenter
a outra lambda. - Retorna o FocusRequester, que é indexado com o valor
selectedTabIndex
no valorfocusRequesters
, da lambda associada à propriedadeenter
.
O código modificado fica assim:
@Composable
fun App(
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
var selectedScreen by rememberSaveable { mutableStateOf(Screen.FocusTarget) }
val selectedTabIndex = Screen.entries.indexOf(selectedScreen)
val focusRequesters = remember {
List(Screen.entries.size) { FocusRequester() }
}
Column(modifier = modifier) {
PrimaryTabRow(
selectedTabIndex = selectedTabIndex,
modifier = Modifier
.focusProperties {
enter = {
focusRequesters[selectedTabIndex]
}
}
.focusGroup()
) {
Screen.entries.forEachIndexed { index, screen ->
Tab(
selected = screen == selectedScreen,
onClick = { selectedScreen = screen },
text = { Text(stringResource(screen.title)) },
modifier = Modifier.focusRequester(focusRequester = focusRequesters[index])
)
}
}
when (selectedScreen) {
Screen.FocusTarget -> {
FocusTargetTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp),
)
}
Screen.FocusTraversalOrder -> {
FocusTraversalOrderTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp)
)
}
Screen.FocusRestoration -> {
FocusGroupTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp)
)
}
}
}
}
É possível controlar o movimento de foco com o modificador focusProperties
. Na lambda transmitida ao modificador, modifique o elemento FocusProperties, que é referenciado quando o sistema escolhe o alvo de foco quando os usuários pressionam a tecla Tab
ou as teclas direcionais quando o elemento da interface modificado está em foco.
Quando você define a propriedade enter
, o sistema avalia a lambda definida para a propriedade e muda para o elemento da interface associado ao objeto FocusRequester
retornado pela lambda avaliada.
Executar
Agora, o foco do teclado se move para a guia selecionada quando o usuário move o foco para a linha de guias. Siga estas etapas para testar:
- Executar o app
- Selecione a guia Focus group
- Mova o foco para o primeiro card com a tecla direcional
down
. - Mova o foco com a tecla direcional
up
.
Figura 13. O foco muda para a guia selecionada.
9. Restauração de foco
Os usuários esperam poder retomar facilmente uma tarefa quando ela é interrompida. A restauração de foco é compatível com a recuperação após uma interrupção. A restauração do foco move o foco do teclado para o elemento da interface que foi selecionado anteriormente.
Um caso de uso típico de restauração de foco é a tela inicial de apps de streaming de vídeo. A tela tem várias listas de conteúdo em vídeo, como filmes em uma categoria ou episódios de um programa de TV. Os usuários procuram nas listas e encontram conteúdo interessante. Às vezes, os usuários voltam para a lista examinada anteriormente e continuam navegando nela. Com a restauração do foco, os usuários podem continuar navegando sem mover o foco do teclado para o último item que analisaram na lista.
O modificador focusRestorer restaura o foco em um grupo de foco
Use o modificador focusRestorer
para salvar e restaurar o foco em um grupo de foco. Quando o foco sai do grupo, ele armazena uma referência ao item que estava focado anteriormente. Quando o foco entra novamente no grupo, ele é restaurado para o item em foco anteriormente.
Integrar a restauração do foco à guia "Focus group"
A guia "Focus group" do app de exemplo tem uma linha com o segundo card, o terceiro e o quarto.
Figura 14. Grupo de discussão com o segundo card, o terceiro e o quarto.
É possível integrar a restauração de foco na linha seguindo estas etapas:
- Abrir
tab.FocusGroupTab.kt
- Modifique o elemento combinável
Row
no combinávelFocusGroupTab
com o modificadorfocusRestorer
. O modificador precisa ser chamado antes do modificadorfocusGroup
.
O código modificado fica assim:
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.focusRestorer()
.focusGroup(),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
Executar
Agora, a linha na guia Focus group restaura o foco. Siga estas etapas para testar:
- Selecione a guia Focus group
- Mova o foco para o primeiro card
- Mova o foco para o quarto card com a tecla
Tab
- Mova o foco para o primeiro card com a tecla direcional
up
- Pressione a tecla
Tab
O foco do teclado muda para o quarto card, porque o modificador focusRestorer
salva a referência do card e restaura o foco quando o foco do teclado entra no grupo de foco definido para a linha.
Figura 15. O foco retorna ao quarto card após a tecla direcional para cima ser pressionada após o pressionamento da tecla Tab
.
10. Criar um teste
É possível testar o gerenciamento de foco do teclado implementado com testes. O Compose fornece uma API para testar se um elemento da interface está com foco e realizar pressionamentos de tecla nos componentes da interface. Consulte o codelab Como testar no Jetpack Compose para mais informações.
Testar a guia "Focus target"
Você modificou a função combinável FocusTargetTab
para definir o segundo card como um alvo de foco na seção anterior. Escreva um teste da implementação que você realizou manualmente na seção anterior. O teste pode ser criado seguindo estas etapas:
- Abra
FocusTargetTabTest.kt
. Você vai modificar a funçãotestSecondCardIsFocusTarget
nas próximas etapas. - Peça para o foco mudar para o primeiro card chamando o método
requestFocus
no objetoSemanticsNodeInteraction
para o primeiro card. - Confira se o primeiro card está focado com o método
assertIsFocused()
. - Realize o pressionamento da tecla
Tab
chamando o métodopressKey
com o valorKey.Tab
dentro da lambda transmitida para o métodoperformKeyInput
. - Teste se o foco do teclado muda para o segundo card chamando o método
assertIsFocused()
no objetoSemanticsNodeInteraction
para o segundo card.
O código atualizado ficará assim:
@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun testSecondCardIsFocusTarget() {
composeTestRule.setContent {
LocalInputModeManager
.current
.requestInputMode(InputMode.Keyboard)
FocusTargetTab(onClick = {})
}
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Ensure the 1st card is focused
composeTestRule
.onNodeWithText(context.getString(R.string.first_card))
.requestFocus()
.performKeyInput { pressKey(Key.Tab) }
// Test if focus moves to the 2nd card from the 1st card with Tab key
composeTestRule
.onNodeWithText(context.getString(R.string.second_card))
.assertIsFocused()
}
Executar
Para executar o teste, clique no ícone triangular mostrado à esquerda da declaração da classe FocusTargetTest
. Consulte a seção Executar testes em Testar no Android Studio para mais informações.
11. Parabéns
Muito bem! Você aprendeu sobre os elementos básicos para o gerenciamento do foco do teclado:
- Alvo de foco
- Transição de foco
É possível controlar a ordem de apresentação de foco com estes modificadores do Compose:
- O modificador
focusGroup
- O modificador
focusProperties
Você implementou o padrão típico de UX com teclado físico, foco inicial e restauração do foco. Esses padrões são implementados combinando estas APIs:
- Classe
FocusRequester
- O modificador
focusRequester
- O modificador
focusRestorer
- A função combinável
LaunchedEffect
A UX implementada pode ser testada com testes instrumentados. O Compose oferece maneiras de realizar pressionamentos de tecla e testar se um SemanticsNode
tem ou não o foco do teclado.