Agrupar alvos de foco

É possível agrupar vários alvos de foco usando o modificador focusGroup. O agrupamento permite gerenciar hierarquias de foco, isolar regiões lógicas da interface (como barras de ferramentas, ações de caixa de diálogo ou listas de cards) e interceptar quando o foco entra ou sai de uma região.

Conceitos de grupo de discussão: entrada, saída e travessia padrão

Um grupo de discussão estabelece um limite de navegação coeso em torno dos alvos de foco filhos. Para entender como o foco se move por esse limite, é preciso conhecer dois conceitos importantes:

  • Enter: o evento que ocorre quando o foco passa de qualquer elemento fora do grupo de foco para um elemento dentro do grupo.
  • Saída: o evento que ocorre quando o foco sai do último elemento interno do grupo e faz a transição para um destino de foco externo.
Diagrama mostrando as transições de entrada e saída em um limite de grupo de discussão.
Figura 1. Grupo de discussão com eventos de entrada e saída.

Comportamento padrão de transição de foco

Por padrão, quando um grupo de discussão recebe foco de fora:

  1. Entrar no grupo: o Compose seleciona automaticamente um destino de foco filho interno. Para a navegação unidimensional (Tab), ele seleciona o primeiro filho na ordem de aparência visual. Para navegação bidimensional (teclas de seta ou botão direcional), ele seleciona o filho que está geometricamente mais próximo da direção de entrada.
  2. Navegação no grupo: pressionar repetidamente as teclas de navegação move o foco entre os alvos de foco irmãos incluídos no grupo.
  3. Sair do grupo: quando o usuário navega além do último destino de foco dentro do grupo, o próximo pressionamento de tecla sai do grupo e move o foco para o destino de foco vizinho fora do grupo.

Personalizar o redirecionamento de entrada de foco

Em muitos designs de interface, o destino de entrada padrão (como o primeiro filho na ordem de exibição) não é a escolha mais lógica ou conveniente para o usuário.

Intenção e casos de uso

Considere uma caixa de diálogo de confirmação com um botão "Cancelar" à esquerda e um botão principal "Confirmar" à direita. Quando o usuário navega para baixo até a barra de botões de um campo de formulário:

  • Comportamento padrão: o foco seria direcionado para "Cancelar" porque é o primeiro botão na ordem de aparência visual.
  • Comportamento pretendido: como "Confirmar" é a principal ação recomendada, talvez você queira que o foco vá diretamente para "Confirmar" ao entrar no grupo de botões da ordem de foco anterior, mas ainda permitindo que o usuário pressione Esquerda para selecionar "Cancelar", se necessário.
Entrada de foco padrão em um grupo de botões.
Figura 2. Botões Cancelar e Confirmar com o destino de entrada de foco padrão.

Outros casos de uso comuns incluem:

  • Mover o foco diretamente para a guia selecionada ao inserir um TabRow.
  • Mover o foco diretamente para o controle "Reproduzir / Pausar" ao entrar em uma barra de reprodução de mídia.

Implementação com onEnter e FocusRequester

Para redirecionar o foco ao entrar em um grupo de discussão:

  1. Aplique o modificador focusGroup ao contêiner pai que inclui os alvos de foco relacionados.
  2. Aplique o modificador focusProperties ao contêiner e configure o callback onEnter.
  3. No callback onEnter (com escopo para FocusEnterExitScope), chame requestFocus() em um FocusRequester associado ao filho padrão preferido usando focusRequester.

val defaultChildRequester = remember { FocusRequester() }

Row(
    modifier = Modifier
        .focusGroup()
        .focusProperties {
            // Intercept entry into this group and redirect focus to the primary action
            onEnter = {
                defaultChildRequester.requestFocus()
            }
        }
) {
    Button(onClick = { /* Cancel action */ }) {
        Text("Cancel")
    }
    Button(
        onClick = { /* Confirm action */ },
        modifier = Modifier.focusRequester(defaultChildRequester)
    ) {
        Text("Confirm (Default)")
    }
}

Dentro de onEnter e onExit, você também pode chamar cancelFocusChange() para bloquear completamente as transições de foco no limite ou inspecionar requestedFocusDirection para personalizar o roteamento com base na navegação do usuário usando Tab, Seta para baixo ou Seta para a esquerda.

Os contêineres roláveis são grupos de foco por padrão

No Jetpack Compose, contêineres roláveis, como LazyRow, LazyColumn e LazyVerticalGrid, atuam automaticamente como grupos de foco. Não é necessário aplicar explicitamente o modificador focusGroup a eles para definir callbacks onEnter ou onExit.