Passaggi principali per migliorare l'accessibilità di Compose

Per aiutare le persone con esigenze di accessibilità a utilizzare correttamente la tua app, progettala in modo da supportare i requisiti di accessibilità principali.

Considera le dimensioni minime dei touch target

Ogni elemento sullo schermo che è possibile selezionare, toccare o con cui è possibile interagire deve essere abbastanza grande da consentire un'interazione affidabile. Quando ridimensioni questi elementi, assicurati di impostare la dimensione minima su 48 dp in modo da rispettare correttamente le linee guida per l'accessibilità di Material Design.

I componenti del materiale, come Checkbox, RadioButton, Switch, Slider e Surface, impostano internamente questa dimensione minima, ma solo quando il componente può ricevere azioni degli utenti. Ad esempio, quando il parametro onCheckedChange di Checkbox è impostato su un valore diverso da null, la casella di controllo include la spaziatura interna in modo da avere larghezza e altezza di almeno 48 dp.

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

Se il parametro onCheckedChange è impostato su null, la spaziatura interna non è inclusa, perché non è possibile interagire direttamente con il componente.

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

Figura 1. Una casella di controllo senza spaziatura interna.

Quando implementi controlli di selezione come Switch, RadioButton o Checkbox, in genere rimuovi il comportamento cliccabile in un contenitore principale, imposti il callback del clic sul componibile su null e aggiungi un modificatore toggleable o selectable all'elemento componibile principale.

@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 le dimensioni di un componibile cliccabile sono inferiori a quelle minime del touch target, Compose aumenta comunque le dimensioni del touch target. Per farlo, espande le dimensioni del touch target al di fuori dei confini del componibile.

L'esempio seguente contiene un elemento Box selezionabile molto piccolo. L'area del touch target viene espansa automaticamente oltre i limiti di Box, quindi il tocco accanto a Box attiva comunque l'evento di clic.

@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)
        )
    }
}

Per evitare possibili sovrapposizioni tra le aree di tocco di diversi componibili, utilizza sempre una dimensione minima abbastanza grande per l'elemento componibile. Nell'esempio, ciò significherebbe utilizzare il modificatore sizeIn per impostare le dimensioni minime della casella 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)
        )
    }
}

Aggiungi etichette clic

Puoi utilizzare un'etichetta clic per aggiungere un significato semantico al comportamento dei clic di un componibile. Le etichette dei clic descrivono cosa succede quando l'utente interagisce con il componibile. I servizi di accessibilità utilizzano le etichette clic per descrivere l'app agli utenti con esigenze specifiche.

Imposta l'etichetta clic passando un parametro nel modificatore 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
        )
    ) {
        // ..
    }
}

In alternativa, se non hai accesso al modificatore cliccabile, imposta l'etichetta clic nel modificatore semantica:

@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)
        }
    ) {
        // ..
    }
}

Descrivere gli elementi visivi

Quando definisci un elemento componibile Image o Icon, il framework Android non ha modo automatico di capire che cosa viene visualizzato nell'app. Devi trasmettere una descrizione testuale dell'elemento visivo.

Immagina una schermata in cui l'utente può condividere la pagina corrente con gli amici. Questa schermata contiene un'icona di condivisione cliccabile:

Una striscia di icone cliccabili, con i

Basandosi sulla sola icona, il framework Android non è in grado di descriverlo a un utente con disabilità visiva. Il framework Android richiede una descrizione testuale aggiuntiva dell'icona.

Il parametro contentDescription descrive un elemento visivo. Utilizza una stringa localizzata, perché è visibile all'utente.

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

Alcuni elementi visivi sono puramente decorativi e potresti non volerli comunicare all'utente. Se imposti il parametro contentDescription su null, dichiari al framework Android che a questo elemento non sono associati azioni o stato.

@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)
    )
}

Spetta a te decidere se un determinato elemento visivo ha bisogno di un contentDescription. Chiediti se l'elemento trasmette le informazioni necessarie all'utente per eseguire l'attività. In caso contrario, è meglio escludere la descrizione.

Unisci elementi

I servizi di accessibilità come TalkBack e Switch Access consentono agli utenti di spostare lo stato attivo tra gli elementi sullo schermo. È importante focalizzare l'attenzione degli elementi con la giusta granularità. Quando ogni singolo elemento componibile di basso livello sullo schermo è incentrato in modo indipendente, gli utenti devono interagire molto per spostarsi sullo schermo. Se gli elementi si uniscono in modo troppo aggressivo, gli utenti potrebbero non capire quali elementi appartengono

Quando applichi un modificatore clickable a un componibile, Compose unisce automaticamente tutti gli elementi che contiene. Questo vale anche per ListItem: gli elementi all'interno di un elemento dell'elenco vengono uniti e i servizi di accessibilità li visualizzano come un unico elemento.

È possibile avere un insieme di elementi componibili che formano un gruppo logico, ma tale gruppo non è cliccabile o non fa parte di un elemento dell'elenco. Vuoi comunque che i servizi di accessibilità li vedano come un singolo elemento. Ad esempio, immagina un componibile che mostra l'avatar di un utente, il suo nome e alcune informazioni aggiuntive:

Un gruppo di elementi dell'interfaccia utente, tra cui il nome di un utente. Il nome è selezionato.

Puoi abilitare Compose per unire questi elementi utilizzando il parametro mergeDescendants nel modificatore semantics. In questo modo, i servizi di accessibilità selezionano solo l'elemento unito e tutte le proprietà semantiche dei discendenti vengono unite.

@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")
        }
    }
}

I servizi di accessibilità ora si concentrano sull'intero container contemporaneamente, unendo i relativi contenuti:

Un gruppo di elementi dell'interfaccia utente, tra cui il nome di un utente. Tutti gli elementi vengono selezionati insieme.

Aggiungi azioni personalizzate

Dai un'occhiata alla seguente voce dell'elenco:

Un tipico elemento di elenco, contenente il titolo dell'articolo, l'autore e l'icona di un segnalibro.

Quando utilizzi uno screen reader come TalkBack per ascoltare ciò che viene visualizzato sullo schermo, viene selezionato prima l'intero elemento, poi l'icona dei preferiti.

L'elemento dell'elenco, con tutti gli elementi selezionati insieme.

L'elemento dell'elenco, con selezionata solo l'icona dei preferiti

In un lungo elenco, questo può diventare molto ripetitivo. Un approccio migliore consiste nel definire un'azione personalizzata che consenta a un utente di aggiungere l'elemento ai preferiti. Tieni presente che dovrai anche rimuovere esplicitamente il comportamento dell'icona dei preferiti per assicurarti che non venga selezionato dal servizio di accessibilità. Per eseguire questa operazione, utilizza il modificatore 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 { }
        )
    }
}

Descrivere lo stato di un elemento

Un componibile può definire un elemento stateDescription per la semantica che il framework Android utilizza per leggere ad alta voce lo stato in cui si trova l'elemento componibile. Ad esempio, un componibile attivabile può essere nello stato "Selezionato" o "Non selezionato". In alcuni casi, è consigliabile sostituire le etichette predefinite delle descrizioni dello stato utilizzate da Compose. Puoi farlo specificando esplicitamente le etichette della descrizione dello stato prima di definire un componibile come attivabile:

@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() }
            )
    ) {
        /* ... */
    }
}

Definisci intestazioni

A volte le app mostrano molti contenuti in una schermata all'interno di un contenitore scorrevole. Ad esempio, una schermata potrebbe mostrare l'intero contenuto di un articolo che l'utente sta leggendo:

Screenshot di un post del blog, con il testo dell'articolo in un contenitore scorrevole.

Gli utenti con esigenze di accessibilità hanno difficoltà a navigare in una schermata di questo tipo. Per facilitare la navigazione, indica quali elementi sono intestazioni. Nell'esempio precedente, ogni titolo di sottosezione potrebbe essere definito come un'intestazione per l'accessibilità. Alcuni servizi di accessibilità, come TalkBack, consentono agli utenti di passare direttamente da un'intestazione all'altra.

In Compose, indichi che un componibile è un'intestazione definendone la proprietà semantics:

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

Gestire i componenti componibili personalizzati

Ogni volta che sostituisci determinati componenti Material nella tua app con versioni personalizzate, devi tenere a mente le considerazioni sull'accessibilità.

Supponiamo che tu stia sostituendo il materiale Checkbox con la tua implementazione. Potresti dimenticare di aggiungere il modificatore triStateToggleable, che gestisce le proprietà di accessibilità di questo componente.

Come regola generale, osserva l'implementazione del componente nella raccolta Material e imita qualsiasi comportamento di accessibilità trovato. Inoltre, fai un uso intensivo dei modificatori di base, anziché dei modificatori a livello di interfaccia utente, perché includono considerazioni sull'accessibilità pronte all'uso.

Testa l'implementazione del componente personalizzato con più servizi di accessibilità per verificarne il comportamento.

Risorse aggiuntive