Gerenciamento de foco do teclado no Compose

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

  1. 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:

  1. Navegue até a pasta focus-management-in-compose.
  2. No Android Studio, abra o projeto. A pasta focus-management-in-compose contém um projeto.
  3. 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.

O Gerenciador de dispositivos do Android Studio mostra a lista de dispositivos virtuais disponíveis na categoria "Phone". O emulador redimensionável está nessa categoria.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.

A primeira visualização do app de exemplo. Ela tem três guias. A primeira guia, "Focus target", está selecionada. A guia mostra três cards em uma coluna.

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:

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.

O arquivo de animação GIF mostra como o foco do teclado se move pelos elementos da interface. Ele se move pelas três guias e, em seguida, o primeiro card 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.

A animação em GIF mostra o movimento inicial do foco do teclado na guia "Focus target". O segundo card é pulado e o foco passa para o terceiro a partir do primeiro quando o usuário pressiona a tecla Tab no primeiro card.

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:

  1. Abra FocusTargetTab.kt no pacote tabs.
  2. Modifique o elemento combinável SecondCard com o modificador clickable 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.

A animação em GIF mostra o movimento do foco do teclado após a modificação. Quando o usuário pressiona a tecla Tab no primeiro card, o foco se move desse card.

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:

  1. Primeiro card
  2. Quarto card
  3. Terceiro card
  4. Segundo card

A animação em GIF mostra que o foco do teclado se move de maneira diferente da expectativa do usuário.  Ele se move do primeiro para o terceiro ,depois para o quarto e para o segundo. Isso pode ser diferente da expectativa do usuário.

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:

  1. Abrir tabs.FocusTraversalOrderTab.kt
  2. Remova o modificador de deslocamento dos elementos combináveis ThirdCard e FourthCard.
  3. Mude o layout da guia para uma coluna com duas linhas da linha atual com duas colunas.
  4. Mova os elementos combináveis FirstCard e SecondCard para a primeira linha.
  5. Mova os elementos combináveis ThirdCard e FourthCard 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:

  1. Primeiro card
  2. Segundo card
  3. Terceiro card
  4. Quarto card

A animação em GIF mostra como o foco do teclado se move após a modificação. Ele se move da esquerda para a direita, de cima para baixo, no padrão em Z.

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.

A animação em GIF mostra o foco do teclado movendo do primeiro card para o terceiro com a tecla de direção para a direita. Esses dois cards estão em linhas diferentes.

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".

O GIF mostra que o foco se move para o primeiro card da guia "Focus target" com a tecla de direção para baixo e depois volta para a guia com a tecla de direção para cima. Esses dois alvos de foco são os mais próximos verticalmente.

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.

O GIF mostra que o foco permanece no segundo card, mesmo que o usuário pressione a tecla de direção para baixo, porque nenhum alvo de foco está abaixo do card.

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:

  1. Abrir tabs.FocusGroup.kt
  2. Modifique a função combinável Column na função combinável FocusGroupTab com o modificador focusGroup.

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.

A animação em GIF mostra que o usuário precisa pressionar a tecla Tab três vezes após selecionar a guia para mover o foco do teclado para o primeiro card na guia.

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:

  1. Abrir tabs.FocusTarget.kt
  2. Declare o valor firstCard na função combinável FocusTargetTab e inicialize o valor com um objeto FocusRequester retornado da função remember.
  3. Modifique a função combinável FirstCard com o modificador focusRequester.
  4. Especifique o valor firstCard como o argumento do modificador focusRequester.
  5. Chame a função combinável LaunchedEffect com o valor Unit e chame o método requestFocus() sobre o valor firstCard na lambda transmitida para a função combinável LaunchedEffect.

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.

A animação em GIF mostra que o foco do teclado se move automaticamente para o primeiro card quando o usuário seleciona a guia "Focus target".

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:

  1. Abra App.kt.
  2. Declare o valor focusRequesters na função combinável App.
  3. Inicialize o valor focusRequesters com o valor de retorno da função remember, que retorna uma lista de objetos FocusRequester. O comprimento da lista retornada precisa ser igual ao de Screens.entries.
  4. Associe cada objeto FocusRequester do valor focusRequester ao elemento combinável Tab modificando o elemento combinável da guia com o modificador focusRequester.
  5. Modifique o elemento combinável PrimaryTabRow com os modificadores focusProperties e focusGroup.
  6. Transmita uma lambda ao modificador focusProperties e associe a propriedade enter a outra lambda.
  7. Retorna o FocusRequester, que é indexado com o valor selectedTabIndex no valor focusRequesters, da lambda associada à propriedade enter.

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:

  1. Executar o app
  2. Selecione a guia Focus group
  3. Mova o foco para o primeiro card com a tecla direcional down.
  4. 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.

A animação em GIF mostra o foco do teclado movendo-se do primeiro para o segundo card, mesmo que o terceiro tenha sido focado anteriormente.

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:

  1. Abrir tab.FocusGroupTab.kt
  2. Modifique o elemento combinável Row no combinável FocusGroupTab com o modificador focusRestorer. O modificador precisa ser chamado antes do modificador focusGroup.

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:

  1. Selecione a guia Focus group
  2. Mova o foco para o primeiro card
  3. Mova o foco para o quarto card com a tecla Tab
  4. Mova o foco para o primeiro card com a tecla direcional up
  5. 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.

A animação em GIF mostra que o foco do teclado se move para o card selecionado anteriormente em uma linha quando o foco do teclado retorna.

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:

  1. Abra FocusTargetTabTest.kt. Você vai modificar a função testSecondCardIsFocusTarget nas próximas etapas.
  2. Peça para o foco mudar para o primeiro card chamando o método requestFocus no objeto SemanticsNodeInteraction para o primeiro card.
  3. Confira se o primeiro card está focado com o método assertIsFocused().
  4. Realize o pressionamento da tecla Tab chamando o método pressKey com o valor Key.Tab dentro da lambda transmitida para o método performKeyInput.
  5. Teste se o foco do teclado muda para o segundo card chamando o método assertIsFocused() no objeto SemanticsNodeInteraction 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.

O Android Studio mostra um menu de contexto para executar o "FocusTargetTabTest".

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.

Saiba mais