Principais etapas para melhorar a acessibilidade do Compose

Para ajudar pessoas com necessidades de acessibilidade a usar seu app, crie um app que ofereça suporte aos principais requisitos de acessibilidade.

Considerar os tamanhos mínimos de área de toque

Todos os elementos mostrados na tela que possam ser clicados, tocados ou com os quais é possível interagir de alguma outra forma precisam ser grandes o suficiente para uma interação confiável. Ao dimensionar esses elementos, defina o tamanho mínimo como 48 dp para seguir corretamente as diretrizes de acessibilidade do Material Design.

Componentes do Material Design, como Checkbox, RadioButton, Switch, Slider e Surface, definem esse tamanho mínimo internamente, mas apenas quando o componente pode receber ações do usuário. Por exemplo, quando uma Checkbox tem o parâmetro onCheckedChange definido como um valor não nulo, a caixa de seleção inclui o padding para ter uma largura e altura de pelo menos 48 dp.

@Composable
private fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

Quando o parâmetro onCheckedChange é definido como nulo, o padding não é incluído, porque não é possível interagir diretamente com o componente.

@Composable
private fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

Figura 1. Uma caixa de seleção sem padding.

Ao implementar controles de seleção, como Switch, RadioButton ou Checkbox, você normalmente aumenta o comportamento clicável para um contêiner pai, define o callback de clique no elemento combinável como null e adiciona um modificador toggleable ou selectable ao elemento pai.

@Composable
private fun CheckableRow() {
    MaterialTheme {
        var checked by remember { mutableStateOf(false) }
        Row(
            Modifier
                .toggleable(
                    value = checked,
                    role = Role.Checkbox,
                    onValueChange = { checked = !checked }
                )
                .padding(16.dp)
                .fillMaxWidth()
        ) {
            Text("Option", Modifier.weight(1f))
            Checkbox(checked = checked, onCheckedChange = null)
        }
    }
}

Quando o tamanho de um elemento clicável é menor que o tamanho mínimo da área de toque, o Compose aumenta o tamanho dessa área. Isso é feito expandindo o tamanho da área de toque para fora dos limites do elemento combinável.

O exemplo a seguir contém um Box clicável muito pequeno. A área da área de toque é expandida automaticamente para além dos limites da Box. Portanto, tocar ao lado de Box ainda aciona o evento de clique.

@Composable
private fun SmallBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .size(1.dp)
        )
    }
}

Para evitar possíveis sobreposições entre áreas de toque de diferentes elementos combináveis, sempre use um tamanho mínimo grande o suficiente para o elemento. No exemplo, isso significa usar o modificador sizeIn para definir o tamanho mínimo da caixa interna:

@Composable
private fun LargeBox() {
    var clicked by remember { mutableStateOf(false) }
    Box(
        Modifier
            .size(100.dp)
            .background(if (clicked) Color.DarkGray else Color.LightGray)
    ) {
        Box(
            Modifier
                .align(Alignment.Center)
                .clickable { clicked = !clicked }
                .background(Color.Black)
                .sizeIn(minWidth = 48.dp, minHeight = 48.dp)
        )
    }
}

Adicionar etiquetas de clique

Você pode usar uma etiqueta de clique para adicionar um significado semântico ao comportamento de clique de uma função que pode ser composta. Os rótulos de clique descrevem o que acontece quando o usuário interage com o elemento combinável. Os serviços de acessibilidade usam marcadores de clique para ajudar a descrever o app para usuários com necessidades específicas.

Defina o rótulo de clique transmitindo um parâmetro no modificador clickable:

@Composable
private fun ArticleListItem(openArticle: () -> Unit) {
    Row(
        Modifier.clickable(
            // R.string.action_read_article = "read article"
            onClickLabel = stringResource(R.string.action_read_article),
            onClick = openArticle
        )
    ) {
        // ..
    }
}

Como alternativa, se você não tiver acesso ao modificador clicável, defina o rótulo de clique no modificador semantics:

@Composable
private fun LowLevelClickLabel(openArticle: () -> Boolean) {
    // R.string.action_read_article = "read article"
    val readArticleLabel = stringResource(R.string.action_read_article)
    Canvas(
        Modifier.semantics {
            onClick(label = readArticleLabel, action = openArticle)
        }
    ) {
        // ..
    }
}

Descrever elementos visuais

Quando você define um elemento combinável Image ou Icon, não há maneira automática para o framework do Android entender o que o app está mostrando. É necessário transmitir uma descrição textual do elemento visual.

Imagine uma tela em que o usuário pode compartilhar a página atual com amigos. Essa tela contém um ícone de compartilhamento clicável:

Uma faixa de ícones clicáveis, com o

Com base apenas no ícone, o framework do Android não pode descrevê-lo para um usuário com deficiência visual. O framework do Android precisa de uma descrição textual adicional do ícone.

O parâmetro contentDescription descreve um elemento visual. Use uma string localizada, já que ela fica visível para o usuário.

@Composable
private fun ShareButton(onClick: () -> Unit) {
    IconButton(onClick = onClick) {
        Icon(
            imageVector = Icons.Filled.Share,
            contentDescription = stringResource(R.string.label_share)
        )
    }
}

Alguns elementos visuais são apenas decorativos, e talvez você não queira comunicá-los ao usuário. Ao definir o parâmetro contentDescription como null, você indica ao framework do Android que esse elemento não tem ações ou estados associados.

@Composable
private fun PostImage(post: Post, modifier: Modifier = Modifier) {
    val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1)

    Image(
        painter = image,
        // Specify that this image has no semantic meaning
        contentDescription = null,
        modifier = modifier
            .size(40.dp, 40.dp)
            .clip(MaterialTheme.shapes.small)
    )
}

Cabe a você decidir se um determinado elemento visual precisa de uma contentDescription. Pergunte a si mesmo se o elemento transmite informações necessárias para o usuário realizar a tarefa. Caso contrário, é melhor deixar a descrição de fora.

Mesclar elementos

Os serviços de acessibilidade, como o TalkBack e o acesso com interruptor, permitem que os usuários coloquem o foco em diferentes elementos na tela. É importante que o foco nos elementos tenha a granularidade correta. Quando cada elemento combinável de baixo nível na tela tem foco independente, os usuários precisam interagir muito para se mover pela tela. Se os elementos se mesclarem de maneira muito agressiva, os usuários poderão não entender quais elementos pertencem juntos.

Quando você aplica um modificador clickable a um elemento combinável, o Compose mescla automaticamente todos os elementos que esse elemento contém. Isso também vale para ListItem. Os elementos de um item de lista são mesclados, e os serviços de acessibilidade os veem como um único elemento.

É possível definir um conjunto de funções que podem ser compostas que formem um grupo lógico, mas esse grupo não será clicável nem fará parte de um item de lista. Você ainda vai querer que os serviços de acessibilidade vissem-os como um 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.

Você pode permitir que o Compose mescle esses elementos usando o parâmetro mergeDescendants no modificador semantics. Dessa forma, os serviços de acessibilidade selecionam apenas o elemento combinado, 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 em todo o contêiner de uma só vez, mesclando o conteúdo:

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

Adicionar ações personalizadas

Veja o seguinte item de lista:

Item de lista típico, contendo um título de artigo, autor e ícone "adicionar aos favoritos".

Quando você usa um leitor de tela como o Talkback para ouvir o que é mostrado, ele primeiro seleciona o item todo e, em seguida, o ícone de favorito.

Item de lista, com todos os elementos selecionados juntos.

Item de lista, com apenas o ícone "adicionar aos favoritos" selecionado.

Em uma lista longa, isso pode ficar muito repetitivo. Uma abordagem melhor é definir uma ação personalizada que permita que o usuário adicione o item aos favoritos. Também é necessário remover explicitamente o comportamento do ícone de favorito para garantir que ele não seja selecionado pelo serviço de acessibilidade. Isso é feito com o modificador clearAndSetSemantics:

@Composable
private fun PostCardSimple(
    /* ... */
    isFavorite: Boolean,
    onToggleFavorite: () -> Boolean
) {
    val actionLabel = stringResource(
        if (isFavorite) R.string.unfavorite else R.string.favorite
    )
    Row(
        modifier = Modifier
            .clickable(onClick = { /* ... */ })
            .semantics {
                // Set any explicit semantic properties
                customActions = listOf(
                    CustomAccessibilityAction(actionLabel, onToggleFavorite)
                )
            }
    ) {
        /* ... */
        BookmarkButton(
            isBookmarked = isFavorite,
            onClick = onToggleFavorite,
            // Clear any semantics properties set on this node
            modifier = Modifier.clearAndSetSemantics { }
        )
    }
}

Descrever o estado de um elemento

Um elemento combinável pode definir uma stateDescription como semântica, que o framework do Android usa para ler o estado em que o elemento se encontra. Por exemplo, um elemento combinável alternável pode estar no estado "marcado" ou "não verificado". Em alguns casos, pode ser necessário substituir os rótulos de descrição de estado padrão usados pelo Compose. Para fazer isso, especifique explicitamente os rótulos de descrição de estado antes de definir um elemento combinável como alternável:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

Definir cabeçalhos

Às vezes, os apps mostram muito conteúdo em uma tela em um contêiner rolável. Por exemplo, uma tela pode mostrar todo o conteúdo de um artigo que o usuário está lendo:

Captura de tela de uma postagem de blog, com o texto de um artigo em um contêiner de rolagem.

Usuários com necessidades de acessibilidade têm dificuldade para navegar nessa tela. Para ajudar na navegação, indique quais elementos são cabeçalhos. No exemplo anterior, cada título de subseção poderia ser definido como um cabeçalho para acessibilidade. Alguns serviços de acessibilidade, como o Talkback, permitem que os usuários naveguem diretamente do título ao cabeçalho.

No Compose, para indicar que um elemento combinável é um cabeçalho, defina a propriedade semantics:

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

Processar elementos combináveis personalizados

Sempre que você substituir determinados componentes do Material Design no seu app por versões personalizadas, tenha em mente as considerações de acessibilidade.

Digamos que você esteja substituindo a Checkbox do Material Design por uma implementação própria. Você pode esquecer de adicionar o modificador triStateToggleable, que processa as propriedades de acessibilidade desse componente.

Como regra geral, observe a implementação do componente na biblioteca do Material Design e imite qualquer comportamento de acessibilidade encontrado. Além disso, use os modificadores do Compose Foundation, e não modificadores no nível da IU, porque eles incluem considerações de acessibilidade prontas.

Teste a implementação do componente personalizado com vários serviços de acessibilidade para verificar o comportamento.

Outros recursos