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 um determinado elemento combinável de texto precisa ser
considerado 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]
Vamos ver um exemplo para entender como as propriedades semânticas são usadas para transmitir o significado de um elemento combinável. 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 ativar/desativar". 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.