Mesclagem e limpeza

À medida que os serviços de acessibilidade navegam pelos elementos na tela, é importante que esses elementos sejam agrupados, separados ou até mesmo ocultos na granularidade correta. Quando cada elemento combinável de baixo nível na tela é destacado de forma independente, os usuários precisam interagir muito para navegar pela tela. Se os elementos forem combinados de forma exagerada, os usuários podem não compreender quais elementos estão agrupados. Se houver elementos na tela que sejam puramente decorativos, eles poderão ser ocultados dos serviços de acessibilidade. Nesses casos, é possível usar as APIs do Compose para mesclar, limpar e ocultar a semântica.

Semântica de mesclagem

Quando você aplica um modificador clickable a um elemento combinável pai, o Compose mescla automaticamente todos os elementos filhos abaixo dele. Para entender como os componentes interativos do Compose Material e do Foundation usam estratégias de mesclagem por padrão, consulte a seção Elementos interativos.

É comum que um componente consista em vários elementos combináveis. Esses combináveis podem formar um grupo lógico e cada um pode conter informações importantes, mas talvez você ainda queira que os serviços de acessibilidade os considerem como um único elemento.

Por exemplo, imagine um elemento combinável que mostre o avatar de um usuário, o nome e algumas informações extras:

Um grupo de elementos da IU, incluindo o nome de um usuário. O nome está selecionado.
Figura 1. Um grupo de elementos da IU, incluindo o nome de um usuário. O nome é selecionado.

Você pode ativar o Compose para mesclar esses elementos usando o parâmetro mergeDescendants no modificador de semântica. Dessa forma, os serviços de acessibilidade tratam o componente como uma entidade, e todas as propriedades semânticas dos descendentes são mescladas:

@Composable
private fun PostMetadata(metadata: Metadata) {
    // Merge elements below for accessibility purposes
    Row(modifier = Modifier.semantics(mergeDescendants = true) {}) {
        Image(
            imageVector = Icons.Filled.AccountCircle,
            contentDescription = null // decorative
        )
        Column {
            Text(metadata.author.name)
            Text("${metadata.date}${metadata.readTimeMinutes} min read")
        }
    }
}

Os serviços de acessibilidade agora se concentram no contêiner todo de uma vez e mesclam o conteúdo dele:

Um grupo de elementos da IU, incluindo o nome de um usuário. Todos os elementos estão selecionados juntos.
Figura 2. Um grupo de elementos da IU, incluindo o nome de um usuário. Todos os elementos estão selecionados juntos.

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. É possível verificar a estratégia de mesclagem de uma propriedade semântica verificando a implementação da mergePolicy em SemanticsProperties.kt. As propriedades podem assumir 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.

Há outros cenários em que você espera que a semântica das crianças seja mesclada a uma mãe, mas isso não acontece. No exemplo abaixo, temos clickable item de lista pai com elementos filhos, e podemos esperar que o elemento pai os mescle:

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

@Composable
private fun ArticleListItem(
    openArticle: () -> Unit,
    addToBookmarks: () -> Unit,
) {

    Row(modifier = Modifier.clickable { openArticle() }) {
        // Merges with parent clickable:
        Icon(
            painter = painterResource(R.drawable.ic_logo),
            contentDescription = "Article thumbnail"
        )
        ArticleDetails()

        // Defies the merge due to its own clickable:
        BookmarkButton(onClick = addToBookmarks)
    }
}

Quando o usuário pressiona o item clickable Row, o artigo é aberto. Aninhado dentro, há um BookmarkButton para adicionar o artigo aos favoritos. Esse botão aninhado aparece como não mesclado, enquanto o restante do conteúdo filho dentro da linha é 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 4. A árvore mesclada contém vários textos em uma lista dentro do nó Row. A árvore não mesclada contém nós separados para cada elemento combinável Text.

Por design, alguns elementos combináveis não são mesclados automaticamente em um elemento pai. Um elemento pai não pode mesclar os filhos quando eles também estão sendo mesclados, definindo mergeDescendants = true explicitamente ou sendo componentes que se mesclam, como botões ou elementos clicáveis. Saber como algumas APIs mesclam ou desafios de mesclagem pode ajudar você a depurar alguns comportamentos potencialmente inesperados.

Use a mesclagem quando os elementos filhos formam um grupo lógico e sensato sob o elemento pai. No entanto, se os filhos aninhados precisarem ajustar ou remover manualmente as próprias semânticas, outras APIs podem ser mais adequadas às suas necessidades (por exemplo, clearAndSetSemantics).

Limpar e definir semântica

Se as informações semânticas precisarem ser completamente apagadas ou substituídas, uma API poderosa para usar é clearAndSetSemantics.

Quando um componente precisa limpar a semântica própria e a semântica dos descendentes, use essa API com uma lambda vazia. Quando a semântica precisar ser substituída, inclua o novo conteúdo dentro do lambda.

Ao limpar com um lambda vazio, as semânticas limpas não são enviadas a nenhum consumidor que use essas informações, como acessibilidade, preenchimento automático ou testes. Ao substituir o conteúdo com clearAndSetSemantics{/*semantic information*/}, as novas semânticas substituem todas as semânticas anteriores do elemento e dos descendentes.

Confira a seguir um exemplo de componente de alternância personalizado, representado por uma linha interativa com um ícone e um texto:

// Developer might intend this to be a toggleable.
// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied,
// a custom description is set, and a Role is applied.

@Composable
fun FavoriteToggle() {
    val checked = remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .toggleable(
                value = checked.value,
                onValueChange = { checked.value = it }
            )
            .clearAndSetSemantics {
                stateDescription = if (checked.value) "Favorited" else "Not favorited"
                toggleableState = ToggleableState(checked.value)
                role = Role.Switch
            },
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = null // not needed here

        )
        Text("Favorite?")
    }
}

Embora o ícone e o texto tenham algumas informações semânticas, juntos eles não indicam que esse componente é um botão de alternância. A mesclagem não é suficiente porque é necessário fornecer mais informações sobre o componente.

Como o snippet acima cria um componente de alternância personalizado, é necessário adicionar a capacidade de alternância, bem como a semântica stateDescription, toggleableState e role. Dessa forma, o status do componente e a ação associada ficam disponíveis. Por exemplo, o TalkBack anuncia "Toque duas vezes para alternar" em vez de "Toque duas vezes para ativar".

Ao limpar a semântica original e definir novas semânticas mais descritivas, os serviços de acessibilidade agora podem identificar que este é um componente comutável que pode alternar o estado.

Ao usar clearAndSetSemantics, considere o seguinte:

  • Como os serviços não recebem informações quando essa API está definida, é melhor usá-la com moderação.
    • As informações semânticas podem ser usadas por agentes de IA e serviços semelhantes para entender a tela. Portanto, elas só devem ser limpas quando necessário.
  • A semântica personalizada pode ser definida na API lambda.
  • A ordem dos modificadores é importante. Essa API limpa toda a semântica que é aplicada depois dela, independentemente de outras estratégias de mesclagem.

Ocultar semântica

Em alguns cenários, os elementos não precisam ser enviados aos serviços de acessibilidade. Talvez as informações adicionais sejam redundantes para acessibilidade ou sejam puramente decorativas e não interativas. Nesses casos, é possível ocultar elementos com a API hideFromAccessibility.

Nos exemplos a seguir, há componentes que podem precisar ser ocultos: uma marca d'água redundante que abrange um componente e um caractere usado para separar informações de forma decorativa.

@Composable
fun WatermarkExample(
    watermarkText: String,
    content: @Composable () -> Unit,
) {
    Box {
        WatermarkedContent()
        // Mark the watermark as hidden to accessibility services.
        WatermarkText(
            text = watermarkText,
            color = Color.Gray.copy(alpha = 0.5f),
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .semantics { hideFromAccessibility() }
        )
    }
}

@Composable
fun DecorativeExample() {
    Text(
        modifier =
        Modifier.semantics {
            hideFromAccessibility()
        },
        text = "A dot character that is used to decoratively separate information, like •"
    )
}

O uso de hideFromAccessibility garante que a marca d'água e a decoração sejam ocultas dos serviços de acessibilidade, mas ainda mantenham a semântica para outros casos de uso, como testes.

Detalhes dos casos de uso

Confira a seguir um resumo de casos de uso para entender como diferenciar claramente as APIs anteriores:

  • Quando o conteúdo não é destinado a ser usado por serviços de acessibilidade:
    • Use hideFromAccessibility quando o conteúdo for possivelmente decorativo ou redundante, mas ainda precisar ser testado.
    • Use clearAndSetSemantics{} com uma lambda vazia quando a semântica de pais e filhos precisar ser limpa para todos os serviços.
    • Use clearAndSetSemantics{/*content*/} com conteúdo dentro da lambda quando a semântica de um componente precisar ser definida manualmente.
  • Quando o conteúdo precisa ser tratado como uma entidade e precisa que todas as informações dos filhos sejam completas:
    • Use descendentes semânticos de mesclagem.
Tabela com casos de uso de API diferenciados.
Figura 5. Tabela com casos de uso de API diferenciados.