Controlar a ordem de travessia

Por padrão, o comportamento do leitor de tela de acessibilidade em um app do Compose é implementado na ordem de leitura esperada, que geralmente é da esquerda para a direita e depois de cima para baixo. No entanto, existem alguns tipos de layouts de apps em que o algoritmo não consegue determinar a ordem de leitura real sem mais dicas. Em apps baseados em visualização, é possível corrigir esses problemas usando as propriedades traversalBefore e traversalAfter. A partir do Compose 1.5, ele oferece uma API igualmente flexível, mas com um novo modelo conceitual.

isTraversalGroup e traversalIndex são propriedades semânticas que permitem controlar a acessibilidade e a ordem de foco do TalkBack em cenários em que o algoritmo de classificação padrão não é apropriado. isTraversalGroup identifica grupos semanticamente importantes, enquanto traversalIndex ajusta a ordem de elementos individuais nesses grupos. Você pode usar isTraversalGroup sozinho ou com traversalIndex para personalização adicional.

Use isTraversalGroup e traversalIndex no app para controlar a ordem de travessia do leitor de tela.

Agrupar elementos com isTraversalGroup

isTraversalGroup é uma propriedade booleana que define se um nó semântica é um grupo de travessia. A função desse tipo de nó é servir como limite ou borda na organização dos filhos do nó.

Definir isTraversalGroup = true em um nó significa que todos os filhos desse nó são visitados antes da mudança para outros elementos. Você pode definir isTraversalGroup em nós focalizáveis que não sejam leitores de tela, como colunas, linhas ou caixas.

O exemplo a seguir usa isTraversalGroup. Ele emite quatro elementos de texto. Os dois elementos à esquerda pertencem a um elemento CardBox, enquanto os dois elementos à direita pertencem a outro elemento CardBox:

// CardBox() function takes in top and bottom sample text.
@Composable
fun CardBox(
    topSampleText: String,
    bottomSampleText: String,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        Column {
            Text(topSampleText)
            Text(bottomSampleText)
        }
    }
}

@Composable
fun TraversalGroupDemo() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is "
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
            topSampleText1,
            bottomSampleText1
        )
        CardBox(
            topSampleText2,
            bottomSampleText2
        )
    }
}

O código produz uma saída semelhante a esta:

Layout com duas colunas de texto, com a coluna esquerda lendo "Esta frase está na coluna esquerda" e a coluna direita lendo "Esta frase está à direita".
Figura 1. Um layout com duas frases (uma na coluna da esquerda e outra na coluna da direita).

Como nenhuma semântica foi definida, o comportamento padrão do leitor de tela é percorrer elementos da esquerda para a direita e de cima para baixo. Devido a esse padrão, o TalkBack lê os fragmentos da frase na ordem errada:

"Esta frase está em" → "Esta frase é" → "a coluna da esquerda". → “à direita”.

Para ordenar os fragmentos corretamente, modifique o snippet original e defina isTraversalGroup como true:

@Composable
fun TraversalGroupDemo2() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is"
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
//      1,
            topSampleText1,
            bottomSampleText1,
            Modifier.semantics { isTraversalGroup = true }
        )
        CardBox(
//      2,
            topSampleText2,
            bottomSampleText2,
            Modifier.semantics { isTraversalGroup = true }
        )
    }
}

Como isTraversalGroup é definido especificamente em cada CardBox, os limites CardBox se aplicam ao classificar os elementos. Nesse caso, o CardBox da esquerda é lido primeiro, seguido do CardBox à direita.

Agora, o TalkBack lê os fragmentos de frases na ordem correta:

"Esta frase está em" → "a coluna da esquerda". → "Esta frase está" → "à direita".

Personalizar ainda mais a ordem de apresentação

traversalIndex é uma propriedade de flutuação que permite personalizar a ordem de travessia do TalkBack. Se agrupar elementos não for suficiente para o TalkBack funcionar corretamente, use traversalIndex com isTraversalGroup para personalizar ainda mais a ordem do leitor de tela.

A propriedade traversalIndex tem as seguintes características:

  • Elementos com valores traversalIndex menores têm prioridade.
  • Pode ser positivo ou negativo.
  • O valor padrão é 0f.
  • Afeta apenas os nós focalizáveis do leitor de tela, por exemplo, elementos na tela, como texto ou botões. Por exemplo, definir apenas traversalIndex em uma coluna não terá efeito, a menos que a coluna também tenha isTraversalGroup definido.

O exemplo abaixo mostra como usar traversalIndex e isTraversalGroup juntos.

Exemplo: exibição da aparência do relógio

Um mostrador de relógio é um cenário comum em que a ordem de travessia padrão não funciona. O exemplo nesta seção é um seletor de horário, em que um usuário pode percorrer os números em um mostrador de relógio e selecionar dígitos para os intervalos de hora e minuto.

Um mostrador de relógio com um seletor de horário acima dele.
Figura 2. Imagem de um mostrador de relógio.

No snippet simplificado abaixo, há uma CircularLayout em que 12 números são desenhados, começando com 12 e se movendo no sentido horário ao redor do círculo:

@Composable
fun ClockFaceDemo() {
    CircularLayout {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier) {
        Text((if (value == 0) 12 else value).toString())
    }
}

Como o mostrador de relógio não é lido logicamente com a ordem padrão "da esquerda para a direita" e de cima para baixo, o TalkBack lê os números fora de ordem. Para corrigir isso, use o valor do contador de incremento, conforme mostrado no snippet a seguir:

@Composable
fun ClockFaceDemo() {
    CircularLayout(Modifier.semantics { isTraversalGroup = true }) {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier.semantics { this.traversalIndex = value.toFloat() }) {
        Text((if (value == 0) 12 else value).toString())
    }
}

Para definir a ordem de travessia corretamente, primeiro torne o CircularLayout um grupo de travessia e defina isTraversalGroup = true. Em seguida, à medida que cada texto de relógio é desenhado no layout, defina o traversalIndex correspondente como o valor do contador.

Como o valor do contador aumenta continuamente, o traversalIndex de cada valor do relógio é maior à medida que os números são adicionados à tela. O valor do relógio 0 tem um traversalIndex de 0, e o valor do relógio 1 tem um traversalIndex de 1. Dessa forma, a ordem em que o TalkBack faz a leitura é definida. Agora, os números dentro de CircularLayout são lidos na ordem esperada.

Como os traversalIndexes que foram definidos são relativos apenas a outros índices dentro do mesmo agrupamento, o restante da ordem da tela foi preservado. Em outras palavras, as mudanças semânticas mostradas no snippet de código anterior só modificam a ordem no mostrador de relógio que tem isTraversalGroup = true definido.

Observe que, sem definir a semântica CircularLayout's como isTraversalGroup = true, as mudanças de traversalIndex ainda se aplicam. No entanto, sem o CircularLayout para vinculá-los, os 12 dígitos do mostrador de relógio serão lidos por último, depois que todos os outros elementos na tela forem acessados. Isso ocorre porque todos os outros elementos têm um traversalIndex padrão de 0f, e os elementos de texto do relógio são lidos depois de todos os outros elementos 0f.

Exemplo: personalizar a ordem de travessia do botão de ação flutuante

Neste exemplo, traversalIndex e isTraversalGroup controlam a ordem de travessia de um botão de ação flutuante (FAB) do Material Design. A base deste exemplo é o seguinte layout:

Um layout com uma barra de apps superior, texto de exemplo, um botão de ação flutuante e
  uma barra de apps na parte de baixo.
Figura 3. Layout com uma barra de apps superior, texto de exemplo, um botão de ação flutuante e uma barra de apps na parte de baixo.

Por padrão, o layout neste exemplo tem a seguinte ordem do TalkBack:

Barra de apps superior → Textos de exemplo 0 a 6 → botão de ação flutuante (FAB) → Barra de apps inferior

Você pode querer que o leitor de tela foque primeiro o FAB. Para definir um traversalIndex em um elemento do Material Design, como um FAB, faça o seguinte:

@Composable
fun FloatingBox() {
    Box(modifier = Modifier.semantics { isTraversalGroup = true; traversalIndex = -1f }) {
        FloatingActionButton(onClick = {}) {
            Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
        }
    }
}

Neste snippet, criar uma caixa com isTraversalGroup definido como true e definir um traversalIndex na mesma caixa (-1f é menor que o valor padrão de 0f) significa que a caixa flutuante vem antes de todos os outros elementos na tela.

Em seguida, coloque a caixa flutuante e outros elementos em um scaffold, que implementa um layout do Material Design:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColumnWithFABFirstDemo() {
    Scaffold(
        topBar = { TopAppBar(title = { Text("Top App Bar") }) },
        floatingActionButtonPosition = FabPosition.End,
        floatingActionButton = { FloatingBox() },
        content = { padding -> ContentColumn(padding = padding) },
        bottomBar = { BottomAppBar { Text("Bottom App Bar") } }
    )
}

O TalkBack interage com os elementos na seguinte ordem:

FAB → Barra de apps superior → Textos de exemplo de 0 a 6 → Barra de apps inferior

Outros recursos