Semântica no Compose

Uma composição descreve a interface do app e é produzida pela execução de elementos combináveis. A composição é uma estrutura de árvore que consiste nos elementos combináveis que descrevem a interface.

Ao lado da composição, há uma árvore paralela, chamada árvore de semântica. Esta árvore descreve a IU de uma maneira alternativa, que é compreensível para os serviços de acessibilidade e para o framework de testes. Os serviços de acessibilidade usam a árvore para descrever o app para usuários com uma necessidade específica. O framework de teste usa a árvore para interagir com o app e fazer declarações sobre ele. A árvore semântica não contém as informações sobre como mostrar os elementos combináveis, mas ela contém informações sobre o significado semântico deles.

Uma hierarquia de interface típica e a respectiva árvore semântica
Figura 1. Uma hierarquia de IU típica e a árvore semântica dela.

Caso o app seja formado por funções que podem ser compostas e modificadores da biblioteca de base do Compose e da biblioteca do Material Design, a árvore semântica será preenchida e gerada automaticamente para você. No entanto, ao adicionar elementos combináveis personalizados de baixo nível, é necessário fornecer a semântica correspondente manualmente. Também pode haver situações em que a árvore não representa o significado dos elementos na tela de forma correta ou completa. Nesse caso, é possível adaptar a árvore.

Considere, por exemplo, esta função que pode ser composta de agenda personalizada:

Um elemento combinável de agenda personalizado com elementos de dia selecionáveis
Figura 2. Um elemento combinável de agenda personalizado com elementos de dia selecionáveis.

Neste exemplo, a agenda toda é implementada como um única função que pode ser composta de baixo nível, usando a função Layout que pode ser composta e exibindo diretamente em Canvas. Se você não fizer mais nada, os serviços de acessibilidade não receberão informações suficientes sobre o conteúdo do elemento combinável e a seleção do usuário na agenda. Por exemplo, se o usuário clicar no dia 17, o framework de acessibilidade receberá apenas as informações de descrição de todo o controle da agenda. Nesse caso, o serviço de acessibilidade do TalkBack anunciaria "Agenda" ou, um pouco melhor, "Agenda de abril", e o usuário ficaria sem saber que dia foi selecionado. Para tornar esse elemento combinável mais acessível, é necessário adicionar informações semânticas manualmente.

Propriedades semânticas

Todos os nós na árvore da IU com algum significado semântico têm um nó paralelo na árvore semântica. O nó na árvore semântica contém essas propriedades que transmitem o significado da função que pode ser composta correspondente. Por exemplo, o elemento combinável Text contém uma propriedade semântica text, porque esse é o significado desse elemento. Uma Icon contém uma propriedade contentDescription, se definida pelo desenvolvedor, que transmite em texto o significado do Icon. Os elementos combináveis e modificadores criados com base na biblioteca fundamental do Compose já definem as propriedades relevantes. Opcionalmente, defina ou substitua as propriedades com os modificadores semantics e clearAndSetSemantics. Por exemplo, adicione ações de acessibilidade personalizadas a um nó, forneça uma descrição de estado alternativa para um elemento alternável ou indique que um determinado elemento de texto precisa ser considerado como título.

Para visualizar a árvore semântica, use a ferramenta Layout Inspector ou o método printToLog() nos testes. Isso mostra a árvore semântica atual dentro do Logcat.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

A saída desse teste seria semelhante a esta:

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

Considere como as propriedades semânticas transmitem o significado de um elemento combinável. Considere um Switch. Para o usuário, ele tem esta aparência:

Figura 3. Uma chave no estado "Ativado" e "Desativado".

Para descrever o significado desse elemento, você pode dizer: "É um switch, que é um elemento alternável no estado "Ativado". Você pode clicar nele para interagir com ele".

É exatamente para isso que servem as propriedades semânticas. O nó semântico desse elemento Switch contém as propriedades abaixo, conforme visualizadas com o Layout Inspector:

Layout Inspector mostrando as propriedades semânticas de um elemento combinável de chave
Figura 4. O Layout Inspector mostrando as propriedades semânticas de um elemento combinável de chave.

O Role indica o tipo de elemento. O StateDescription descreve como o estado "Ativado" precisa ser referenciado. Por padrão, essa é uma versão localizada da palavra "Ativado", mas ela pode ser mais específica (por exemplo, "Ativado") com base no contexto. O ToggleableState é o estado atual do interruptor. A propriedade OnClick faz referência ao método usado para interagir com esse elemento. Para conferir uma lista completa de propriedades semânticas, consulte o objeto SemanticsProperties. Para ver uma lista completa das possíveis ações de acessibilidade, consulte o objeto SemanticsActions.

O monitoramento das propriedades semânticas de cada função que pode ser composta no app resulta em muitas possibilidades eficientes. Alguns exemplos:

  • O TalkBack usa as propriedades para ler em voz alta o que é mostrado na tela e permite que o usuário interaja facilmente com ele. Para o elemento combinável "Switch", o TalkBack pode dizer: "Ativado; Chave; toque duas vezes para alternar". O usuário pode tocar duas vezes na tela para desativar a opção.
  • O framework de testes usa as propriedades para encontrar nós, interagir com eles e fazer declarações. Este é um exemplo de teste do switch:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Árvores semânticas mescladas e não mescladas

Como mencionado anteriormente, é possível ter zero ou mais propriedades definidas para cada função que pode ser composta na árvore da IU. Quando um elemento combinável não tem propriedades semânticas definidas, ele não é incluído como parte da árvore semântica. Dessa forma, a árvore semântica contém apenas os nós que realmente têm significado semântico. No entanto, algumas vezes pode ser útil mesclar subárvores de nós específicas e tratá-las como uma só para transmitir o significado correto do conteúdo exibido na tela. Dessa forma, é possível pensar sobre um conjunto de nós como um todo, em vez de lidar com cada nó descendente individualmente. Como regra geral, cada nó da árvore representa um elemento focalizável ao usar os serviços de acessibilidade.

Um exemplo desse tipo de elemento combinável é Button. Pense em um botão como um único elemento, mesmo que ele contenha vários nós filhos:

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

Na árvore semântica, as propriedades dos descendentes do botão são mescladas, e o botão é apresentado como um único nó de folha na árvore:

Representação semântica de folha única mesclada
Figura 5. Representação semântica de folha única mesclada.

As funções que podem ser compostas e os modificadores podem indicar que querem mesclar as propriedades semânticas dos descendentes chamando o método Modifier.semantics (mergeDescendants = true) {}. Definir essa propriedade como true indica que as propriedades semânticas precisam ser mescladas. No exemplo do Button, o elemento combinável Button usa o modificador clickable internamente, que inclui o modificador semantics. Portanto, os nós descendentes do botão estão mesclados. Leia a documentação de acessibilidade para saber mais sobre quando mudar o comportamento de mesclagem do elemento combinável.

Vários modificadores e funções que podem ser compostas nas bibliotecas Compose Foundation e Compose Material têm essa propriedade definida. Por exemplo, os modificadores clickable e toggleable mesclam os descendentes automaticamente. Além disso, a função ListItem mesclará os descendentes.

Inspecionar as árvores

A árvore semântica é, na verdade, duas árvores diferentes. Há uma árvore semântica mesclada, que mescla os nós descendentes quando mergeDescendants é definido como true. Há também uma árvore semântica não mesclada, que não aplica a mesclagem, mas mantém todos os nós intactos. Os serviços de acessibilidade usam a árvore não mesclada e aplicam os próprios algoritmos de mesclagem, considerando a propriedade mergeDescendants. O framework de teste usa a árvore mesclada por padrão.

É possível inspecionar as duas árvores usando o método printToLog(). Por padrão, e como nos exemplos anteriores, a árvore mesclada é registrada. Para exibir a árvore não mesclada, defina o parâmetro useUnmergedTree do matcher onRoot() como true:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

O Layout Inspector permite mostrar a árvore semântica mesclada e a não mesclada, selecionando a árvore de preferência no filtro de visualização:

Opções de visualização do Layout Inspector, permitindo a exibição da árvore semântica mesclada e não mesclada
Figura 6. Opções de visualização do Layout Inspector, permitindo a exibição da árvore semântica mesclada e não mesclada.

O Layout Inspector mostra a semântica mesclada e a semântica definida de cada nó da árvore no painel de propriedades:

Propriedades semânticas mescladas e definidas
Figura 7. Propriedades semânticas mescladas e definidas.

Por padrão, os matchers do framework de testes usam a árvore semântica mesclada. É por isso que você pode interagir com uma Button fazendo a correspondência do texto mostrado nele:

composeTestRule.onNodeWithText("Like").performClick()

Modifique esse comportamento definindo o parâmetro useUnmergedTree dos matchers como true, assim como o matcher onRoot.

Comportamento de mesclagem

Quando uma função que pode ser composta indica que os descendentes precisam ser mesclados, como a mesclagem ocorre exatamente?

Cada propriedade semântica tem uma estratégia de mesclagem definida. Por exemplo, a propriedade ContentDescription adiciona todos os valores descendentes de ContentDescription a uma lista. Verifique a estratégia de mesclagem de uma propriedade semântica verificando a implementação de mergePolicy em SemanticsProperties.kt. As propriedades podem usar o valor pai ou filho, mesclar os valores em uma lista ou string, não permitir a mesclagem e gerar uma exceção ou qualquer outra estratégia de mesclagem personalizada.

É importante observar que os descendentes que definiram mergeDescendants = true não são incluídos na mesclagem. Confira um exemplo:

Item da lista com imagem, texto e um ícone de favorito
Figura 8. Item da lista com imagem, texto e um ícone de favorito.

Veja um item de lista clicável. Quando o usuário pressiona a linha, o app navega para a página de detalhes do artigo, onde é possível ler o artigo. Dentro do item da lista, há um botão para adicionar a matéria aos favoritos, formando um elemento clicável aninhado para que o botão apareça separadamente na árvore mesclada. O restante do conteúdo da linha será mesclado:

Árvore mesclada contém vários textos em uma lista dentro do nó "Row" (linha). A árvore não mesclada contém nós separados para cada função de texto que pode ser composta.
Figura 9. Árvore mesclada contém vários textos em uma lista dentro do nó "Row" (linha). A árvore não mesclada contém nós separados para cada elemento combinável de texto.

Adaptar a árvore semântica

Como mencionado anteriormente, é possível substituir ou limpar algumas propriedades semânticas ou mudar o comportamento de mesclagem da árvore. Isso é especialmente relevante quando você está criando seus próprios componentes personalizados. Sem definir as propriedades e o comportamento de mesclagem corretos, o app pode não ser acessível e os testes podem se comportar de maneira diferente do esperado. Para ler mais sobre alguns casos de uso comuns em que é preciso adaptar a árvore semântica, leia a documentação de acessibilidade. Se você quiser saber mais sobre testes, confira o guia de testes.

Outros recursos