Semântica no Compose

Uma composição descreve a IU do app e é criada executando funções que podem ser compostas. A composição é uma estrutura de árvore formada pelas funções que podem ser compostas que descrevem a IU.

Ao lado da composição, existe uma árvore paralela, chamada de árvore semântica. Essa árvore descreve a IU de uma forma 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 necessidades específicas. Já o framework de testes usa a árvore para interagir com o app e fazer declarações sobre ele. A árvore semântica não contém informações sobre como mostrar os elementos que podem ser compostos, mas sim sobre o significado semântico deles.

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 funções que podem ser compostas personalizadas de baixo nível, será necessário informar 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:

Figura 2. Função que pode ser composta de agenda personalizada 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 da função que pode ser composta 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 apenas "Agenda" ou, em uma hipótese um pouco melhor, "Agenda de abril", mas o usuário ficaria sem saber que dia foi selecionado. Para tornar essa função que pode ser composta mais acessível, será 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, a função Text que pode ser composta contém uma propriedade semântica text, porque esse é o significado da função. Um Icon contém uma propriedade contentDescription, se definida pelo desenvolvedor, que transmite em texto o significado do Icon. Os elementos que podem ser compostos e os modificadores criados de acordo com a biblioteca Compose Foundation já definem as propriedades relevantes para você. Como alternativa, você pode definir ou substituir as propriedades pelos modificadores semantics e clearAndSetSemantics. Por exemplo, é possível adicionar ações de acessibilidade personalizadas a um nó, fornecer uma descrição de estado alternativa para um elemento alternável ou indicar que uma determinada função de texto que pode ser composta precisa ser considerada como um título.

Para visualizar a árvore semântica, é possível usar a ferramenta Layout Inspector ou o método printToLog() nos testes. Isso exibirá a árvore semântica atual no 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]

Vejamos um exemplo para entender como as propriedades semânticas são usadas para transmitir o significado de um elemento que pode ser composto. Considere um Switch. Para o usuário, ele tem esta aparência:

Figura 3: Um interruptor no estado "Ativado" e "Desativado".

O seguinte poderia ser dito para descrever o significado desse elemento: "Esssa é uma chave, um elemento alternável, atualmente no estado 'Ativado'. Você pode clicar na chave para interagir com ela".

É exatamente para isso que servem as propriedades semânticas. Na visualização do Layout Inspector, o nó semântico desse elemento de interruptor contém estas propriedades:

Figura 4. O Layout Inspector mostrando as propriedades semânticas de uma função de interruptor que pode ser composta.

O atributo Role indica o tipo de elemento que estamos vendo. A StateDescription descreve como o estado "Ativado" precisa ser referenciado. Por padrão, essa descrição é somente uma versão localizada da palavra "Ativado", mas ela pode ser mais específica de acordo com o contexto (por exemplo, "Ligado"). O ToggleableState é o estado atual do interruptor. A propriedade OnClick referencia o método usado para interagir com esse elemento. Para ver 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 é exibido na tela e permite que o usuário interaja facilmente com esse conteúdo. Para o interruptor em questão, ele poderia dizer: "Ativado; Interruptor, toque duas vezes para alternar". O usuário poderia tocar duas vezes na tela a fim de mudar para o estado "Desativado".
  • O framework de testes usa as propriedades para encontrar nós, interagir com eles e fazer declarações. Um exemplo de teste para o interruptor poderia ser:
    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 não há propriedades semânticas definidas para uma função que pode ser composta, ela não é incluída 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, podemos considerar um conjunto de nós como um todo, em vez de processar cada nó descendente de forma individual. Como regra geral, cada nó da árvore representa um elemento focalizável ao usar os serviços de acessibilidade.

Um exemplo dessa função que pode ser composta é o "Button" (botão). Gostaríamos de considerar o 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:

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 Button que pode ser composto usa o modificador clickable internamente, que inclui o modificador semantics. Portanto, os nós descendentes do botão serão mesclados. Leia a documentação de acessibilidade para saber mais sobre quando mudar o comportamento de mesclagem da função que pode ser composta.

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.

Como inspecionar as árvores

Ao tratar da árvore semântica, estamos nos referindo a duas árvores diferentes. Há uma árvore semântica mesclada, que mescla os nós descendentes quando o atributo 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. Por padrão, o framework de teste usa a árvore mesclada.

É possível inspecionar as duas árvores usando o método printToLog(). Por padrão, e como nos exemplos anteriores, a árvore mesclada será 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 exibir a árvore semântica mesclada e a não mesclada, selecionando a árvore de preferência no filtro de visualização:

Figura 5. 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:

Por padrão, os matchers do framework de testes usam a árvore semântica mesclada. É por isso que é possível interagir com um botão fazendo a correspondência do texto mostrado nele:

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

Para substituir esse comportamento, defina o parâmetro useUnmergedTree dos matchers como true, da forma que fizemos anteriormente com 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 da ContentDescription a uma lista. É possível verificar a estratégia de mesclagem de uma propriedade semântica consultando a implementação da mergePolicy em SemanticsProperties.kt. As propriedades podem optar por: sempre escolher o valor pai ou filho, mesclar os valores em uma lista ou string, não permitir a mesclagem de nenhum tipo 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. Vejamos um exemplo:

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

Vejamos 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 o artigo aos favoritos. Nesse caso, temos um elemento clicável aninhado e, por isso, o botão será exibido separadamente na árvore mesclada. O restante do conteúdo da linha será mesclado:

Figura 7. Á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.

Como 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 para criar 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, consulte a documentação de acessibilidade. Para saber mais sobre testes, confira o Guia de testes.