Mudar o comportamento de foco

Às vezes, é necessário substituir o comportamento de foco padrão dos elementos na tela. Por exemplo, talvez você queira agrupar elementos combináveis, impedir o foco em um determinado elemento, solicitar foco em um deles, capturar ou liberar foco ou redirecionar o foco na entrada ou saída. Esta seção descreve como mudar o comportamento do foco quando os padrões não são o que você precisa.

Oferecer uma navegação coerente com grupos de discussão

Às vezes, o Jetpack Compose não adivinha imediatamente o próximo item correto para a navegação com guias, especialmente quando Composables pai complexo, como guias e listas, entra em jogo.

Embora a pesquisa de foco normalmente siga a ordem de declaração do Composables, isso é impossível em alguns casos, como quando um dos Composables na hierarquia é um elemento rolável horizontal que não é totalmente visível. Isso é mostrado no exemplo abaixo.

O Jetpack Compose pode decidir focar o próximo item mais próximo do início da tela, conforme mostrado abaixo, em vez de continuar no caminho esperado para a navegação unidirecional:

Animação de um app mostrando uma navegação horizontal superior e uma lista de itens abaixo.
Figura 1. Animação de um app mostrando uma navegação horizontal superior e uma lista de itens abaixo.

Neste exemplo, está claro que os desenvolvedores não pretendiam que o foco fosse da guia Chocolates para a primeira imagem abaixo e, em seguida, voltasse para a guia Pastéis. Em vez disso, eles queriam que o foco continuasse nas guias até a última guia e, em seguida, se concentrasse no conteúdo interno:

Animação de um app mostrando uma navegação horizontal superior e uma lista de itens abaixo.
Figura 2. Animação de um app mostrando uma navegação horizontal superior e uma lista de itens abaixo.

Em situações em que é importante que um grupo de elementos combináveis receba foco sequencialmente, como na linha da guia do exemplo anterior, você precisa envolver Composable em um pai que tenha o modificador focusGroup():

LazyVerticalGrid(columns = GridCells.Fixed(4)) {
    item(span = { GridItemSpan(maxLineSpan) }) {
        Row(modifier = Modifier.focusGroup()) {
            FilterChipA()
            FilterChipB()
            FilterChipC()
        }
    }
    items(chocolates) {
        SweetsCard(sweets = it)
    }
}

A navegação bidirecional procura o elemento combinável mais próximo da direção fornecida. Se um elemento de outro grupo estiver mais próximo de um item não totalmente visível no grupo atual, a navegação escolherá o mais próximo. Para evitar esse comportamento, você pode aplicar o modificador focusGroup().

FocusGroup faz com que um grupo inteiro pareça uma única entidade em termos de foco, mas o grupo em si não vai receber o foco. Em vez disso, o filho mais próximo vai ganhar foco. Dessa forma, a navegação sabe ir até o item não totalmente visível antes de sair do grupo.

Nesse caso, as três instâncias de FilterChip serão focadas antes dos itens SweetsCard, mesmo quando a SweetsCards estiver completamente visível para o usuário e alguns FilterChip puderem estar ocultos. Isso acontece porque o modificador focusGroup instrui o gerenciador de foco a ajustar a ordem em que os itens são focados para que a navegação seja mais fácil e coerente com a interface.

Sem o modificador focusGroup, se o FilterChipC não estivesse visível, a navegação de foco o selecionaria por último. No entanto, a adição desse modificador o torna não apenas detectável, mas também recebe foco logo após FilterChipB, como os usuários esperam.

Como tornar um elemento combinável focalizável

Alguns elementos combináveis são focalizáveis por design, como um botão ou um elemento combinável com o modificador clickable anexado. Para adicionar especificamente um comportamento focalizável a um elemento combinável, use o modificador focusable:

var color by remember { mutableStateOf(Green) }
Box(
    Modifier
        .background(color)
        .onFocusChanged { color = if (it.isFocused) Blue else Green }
        .focusable()
) {
    Text("Focusable 1")
}

Tornar um elemento combinável fora de foco

Há situações em que alguns dos seus elementos não podem participar do foco. Nessas raras ocasiões, você pode usar canFocus property para excluir uma Composable de ser focalizável.

var checked by remember { mutableStateOf(false) }

Switch(
    checked = checked,
    onCheckedChange = { checked = it },
    // Prevent component from being focused
    modifier = Modifier
        .focusProperties { canFocus = false }
)

Solicitar foco do teclado com FocusRequester

Em alguns casos, pode ser necessário solicitar explicitamente o foco como uma resposta a uma interação do usuário. Por exemplo, você pode perguntar a um usuário se ele quer reiniciar o preenchimento de um formulário e, se ele pressionar "sim", você quer mudar o foco para o primeiro campo do formulário.

A primeira coisa a fazer é associar um objeto FocusRequester ao elemento combinável para que você quer mover o foco do teclado. No snippet de código abaixo, um objeto FocusRequester é associado a um TextField definindo um modificador chamado Modifier.focusRequester:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Você pode chamar o método requestFocus do FocusRequester para enviar solicitações de foco reais. Invoque esse método fora de um contexto Composable. Caso contrário, ele será executado novamente a cada recomposição. O snippet abaixo mostra como solicitar que o sistema mova o foco do teclado quando o botão for clicado:

val focusRequester = remember { FocusRequester() }
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.focusRequester(focusRequester)
)

Button(onClick = { focusRequester.requestFocus() }) {
    Text("Request focus on TextField")
}

Capturar e liberar foco

É possível aproveitar o foco para orientar os usuários a fornecer os dados certos que o app precisa para executar a tarefa, por exemplo, conseguir um endereço de e-mail ou número de telefone válido. Embora os estados de erro informem aos usuários o que está acontecendo, pode ser necessário manter o foco do campo com informações incorretas até que ele seja corrigido.

Para capturar o foco, você pode invocar o método captureFocus() e liberá-lo depois com o método freeFocus(), como no exemplo a seguir:

val textField = FocusRequester()

TextField(
    value = text,
    onValueChange = {
        text = it

        if (it.length > 3) {
            textField.captureFocus()
        } else {
            textField.freeFocus()
        }
    },
    modifier = Modifier.focusRequester(textField)
)

Precedência de modificadores de foco

Modifiers podem ser vistos como elementos que têm apenas um filho. Portanto, ao colocá-los na fila, cada Modifier à esquerda (ou de cima) encapsula a Modifier seguinte à direita (ou abaixo). Isso significa que a segunda Modifier está dentro da primeira. Assim, ao declarar dois focusProperties, apenas o primeiro funciona, já que os seguintes estão contidos no primeiro.

Para esclarecer mais o conceito, consulte o código a seguir:

Modifier
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

Nesse caso, o focusProperties que indica item2 como o foco correto não será usado, porque está contido no anterior. Portanto, item1 será usado.

Aproveitando essa abordagem, um pai também pode redefinir o comportamento para o padrão usando FocusRequester.Default:

Modifier
    .focusProperties { right = Default }
    .focusProperties { right = item1 }
    .focusProperties { right = item2 }
    .focusable()

O pai não precisa fazer parte da mesma cadeia de modificadores. Um elemento combinável pai pode substituir uma propriedade de foco de um elemento filho. Por exemplo, considere este FancyButton que torna o botão não focalizável:

@Composable
fun FancyButton(modifier: Modifier = Modifier) {
    Row(modifier.focusProperties { canFocus = false }) {
        Text("Click me")
        Button(onClick = { }) { Text("OK") }
    }
}

Um usuário pode tornar esse botão focalizável novamente, definindo canFocus como true:

FancyButton(Modifier.focusProperties { canFocus = true })

Como cada Modifier, os relacionados ao foco se comportam de maneira diferente com base na ordem que você os declara. Por exemplo, um código como o seguinte torna a Box focável, mas a FocusRequester não é associada a essa focalizável, já que é declarada após a focalizável.

Box(
    Modifier
        .focusable()
        .focusRequester(Default)
        .onFocusChanged {}
)

É importante lembrar que um focusRequester está associado ao primeiro foco abaixo dele na hierarquia. Portanto, esse focusRequester aponta para o primeiro filho focalizável. Caso não haja nenhuma disponível, nada acontecerá. No entanto, como o Box é focalizável (graças ao modificador focusable()), você pode navegar até ele usando navegação bidirecional.

Como outro exemplo, qualquer uma das opções abaixo funcionaria, já que o modificador onFocusChanged() se refere ao primeiro elemento focalizável que aparece após os modificadores focusable() ou focusTarget().

Box(
    Modifier
        .onFocusChanged {}
        .focusRequester(Default)
        .focusable()
)
Box(
    Modifier
        .focusRequester(Default)
        .onFocusChanged {}
        .focusable()
)

Redirecionar o foco na entrada ou saída

Às vezes, você precisa fornecer um tipo muito específico de navegação, como o mostrado na animação abaixo:

Animação de uma tela mostrando duas colunas de botões lado a lado e o foco de uma coluna para outra.
Figura 3. Animação de uma tela mostrando duas colunas de botões lado a lado e o foco de uma coluna para outra.

Antes de aprender a criar esse comportamento, é importante entender o comportamento padrão da pesquisa de foco. Sem qualquer modificação, quando a pesquisa de foco chegar ao item Clickable 3, pressionar DOWN no botão direcional (ou a tecla de seta equivalente) moverá o foco para o que for exibido abaixo da Column, saindo do grupo e ignorando o da direita. Se não houver itens focalizáveis disponíveis, o foco não se moverá para nenhum lugar, mas permanecerá em Clickable 3.

Para mudar esse comportamento e oferecer a navegação pretendida, você pode usar o modificador focusProperties, que ajuda a gerenciar o que acontece quando a pesquisa de foco entra ou sai da Composable:

val otherComposable = remember { FocusRequester() }

Modifier.focusProperties {
    exit = { focusDirection ->
        when (focusDirection) {
            Right -> Cancel
            Down -> otherComposable
            else -> Default
        }
    }
}

É possível direcionar o foco para uma Composable específica sempre que ela entrar ou sair de uma determinada parte da hierarquia. Por exemplo, quando a interface tiver duas colunas e você quiser garantir que, sempre que a primeira for processada, o foco mude para a segunda:

Animação de uma tela mostrando duas colunas de botões lado a lado e o foco de uma coluna para outra.
Figura 4. Animação de uma tela mostrando duas colunas de botões lado a lado e o foco de uma coluna para outra.

Neste gif, assim que o foco atinge Clickable 3 Composable na Column 1, o próximo item em foco é Clickable 4 em outra Column. Esse comportamento pode ser alcançado combinando o focusDirection com os valores enter e exit dentro do modificador focusProperties. Ambos precisam de uma lambda que use como parâmetro a direção de origem do foco e retorne uma FocusRequester. Essa lambda pode se comportar de três maneiras diferentes: retornar FocusRequester.Cancel impede que o foco continue, enquanto FocusRequester.Default não muda o comportamento. Em vez disso, fornecer o FocusRequester anexado a outro Composable faz o foco pular para essa Composable específica.

Mudar a direção de avanço do foco

Para avançar o foco para o próximo item ou para uma direção precisa, você pode aproveitar o modificador onPreviewKey e sugerir o LocalFocusManager para avançar o foco com o modificador moveFocus.

O exemplo abaixo mostra o comportamento padrão do mecanismo de foco: quando um pressionamento de tecla tab é detectado, o foco avança para o próximo elemento na lista de foco. Embora isso não seja algo que você geralmente precisa configurar, é importante conhecer o funcionamento interno do sistema para poder mudar o comportamento padrão.

val focusManager = LocalFocusManager.current
var text by remember { mutableStateOf("") }

TextField(
    value = text,
    onValueChange = { text = it },
    modifier = Modifier.onPreviewKeyEvent {
        when {
            KeyEventType.KeyUp == it.type && Key.Tab == it.key -> {
                focusManager.moveFocus(FocusDirection.Next)
                true
            }

            else -> false
        }
    }
)

Neste exemplo, a função focusManager.moveFocus() avança o foco para o item especificado ou para a direção implícita no parâmetro da função.