Unioni e sgomberi

Mentre i servizi di accessibilità navigano tra gli elementi sullo schermo, è importante che questi elementi siano raggruppati, separati o persino nascosti con la granularità corretta. Quando ogni singolo componibile di basso livello sullo schermo viene evidenziato 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 logicamente insieme. Se sullo schermo sono presenti elementi puramente decorativi, questi potrebbero essere nascosti ai servizi di accessibilità. In questi casi, puoi utilizzare le API di Compose per unire, cancellare e nascondere la semantica.

Unisci semantica

Quando applichi un modificatore clickable a un componibile principale, Compose unisce automaticamente tutti gli elementi secondari al suo interno. Per capire in che modo i componenti interattivi di Compose Material e Foundation utilizzano le strategie di unione per impostazione predefinita, consulta la sezione Elementi interattivi.

È comune che un componente sia costituito da più componibili. Questi componibili potrebbero formare un gruppo logico e ciascuno potrebbe contenere informazioni importanti, ma potresti comunque voler che i servizi di accessibilità li visualizzino come un unico elemento.

Ad esempio, pensa a un composable che mostra l'avatar di un utente, il suo nome e alcune informazioni aggiuntive:

Un gruppo di elementi UI, incluso il nome di un utente. Il nome è selezionato.
Figura 1. Un gruppo di elementi UI, incluso il nome di un utente. Il nome è selezionato.

Puoi attivare Compose per unire questi elementi utilizzando il parametro mergeDescendants nel modificatore di semantica. In questo modo, i servizi di accessibilità trattano il componente come un'unica entità 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 e ne uniscono i contenuti:

Un gruppo di elementi UI, incluso il nome di un utente. Tutti gli elementi vengono selezionati insieme.
Figura 2. Un gruppo di elementi UI, incluso il nome di un utente. Tutti gli elementi vengono selezionati insieme.

Ogni proprietà di semantica ha una strategia di unione definita. Ad esempio, la proprietà ContentDescription aggiunge tutti i valori ContentDescription discendenti a un elenco. Puoi controllare la strategia di unione di una proprietà di semantica controllando la sua implementazione mergePolicy in SemanticsProperties.kt. Le proprietà possono assumere il valore principale o secondario, unire i valori in un elenco o in una stringa, non consentire l'unione e generare un'eccezione o qualsiasi altra strategia di unione personalizzata.

Esistono altri scenari in cui ti aspetti che la semantica secondaria venga unita a quella principale, ma ciò non accade. Nell'esempio seguente, abbiamo un elemento principale dell'elenco clickable con elementi secondari e potremmo aspettarci che l'elemento principale li unisca tutti:

Voce di elenco con immagine, testo e un'icona di segnalibro
Figura 3. Elemento dell'elenco con immagine, testo e un'icona di segnalibro.

@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 l'utente preme l'elemento clickable Row, l'articolo si apre. All'interno è presente un BookmarkButton per aggiungere l'articolo ai preferiti. Questo pulsante nidificato viene visualizzato come non unito, mentre il resto dei contenuti secondari all'interno della riga è unito:

L'albero unito contiene più testi in un elenco all'interno del nodo Row. L'albero non unito contiene nodi separati per ogni elemento componibile Text.
Figura 4. L'albero unito contiene più testi in un elenco all'interno del nodo Row. L'albero non unito contiene nodi separati per ogni Text composable
.

Alcuni elementi componibili non vengono uniti automaticamente in un elemento principale, per impostazione predefinita. Un elemento principale non può unire i suoi elementi secondari quando anche questi ultimi vengono uniti, sia impostando esplicitamente mergeDescendants = true sia essendo componenti che si uniscono, come pulsanti o elementi cliccabili. Sapere come determinate API si uniscono o non si uniscono può aiutarti a eseguire il debug di alcuni comportamenti potenzialmente imprevisti.

Utilizza l'unione quando gli elementi secondari costituiscono un gruppo logico e sensato all'interno dell'elemento principale. Tuttavia, se gli elementi secondari nidificati richiedono la regolazione manuale o la rimozione della propria semantica, altre API potrebbero essere più adatte alle tue esigenze (ad esempio, clearAndSetSemantics).

Cancella e imposta la semantica

Se le informazioni semantiche devono essere completamente cancellate o sovrascritte, un'API efficace da utilizzare è clearAndSetSemantics.

Quando è necessario cancellare la semantica di un componente e dei suoi discendenti, utilizza questa API con una funzione lambda vuota. Quando la semantica deve essere sovrascritta, includi i nuovi contenuti all'interno della funzione lambda.

Tieni presente che, quando esegui la cancellazione con una lambda vuota, la semantica cancellata non viene inviata a nessun consumer che utilizza queste informazioni, come l'accessibilità, la compilazione automatica o i test. Quando sovrascrivi i contenuti con clearAndSetSemantics{/*semantic information*/}, la nuova semantica sostituisce tutta la semantica precedente dell'elemento e dei relativi discendenti.

Di seguito è riportato un esempio di un componente di attivazione/disattivazione personalizzato, rappresentato da una riga interattiva con un'icona e un testo:

// 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?")
    }
}

Sebbene l'icona e il testo contengano alcune informazioni semantiche, insieme non indicano che questo componente è attivabile/disattivabile. L'unione non è sufficiente perché devi fornire informazioni aggiuntive sul componente.

Poiché lo snippet riportato sopra crea un componente di attivazione/disattivazione personalizzato, devi aggiungere la funzionalità di attivazione/disattivazione, nonché la semantica stateDescription, toggleableState e role. In questo modo, lo stato del componente e l'azione associata sono disponibili. Ad esempio, TalkBack annuncia "Tocca due volte per attivare/disattivare" anziché "Tocca due volte per attivare".

Cancellando la semantica originale e impostandone una nuova e più descrittiva, i servizi di accessibilità possono ora vedere che si tratta di un componente attivabile/disattivabile che può alternare lo stato.

Quando utilizzi clearAndSetSemantics, tieni presente quanto segue:

  • Poiché i servizi non ricevono informazioni quando questa API è impostata, è meglio utilizzarla con parsimonia.
    • Le informazioni semantiche possono essere utilizzate da agenti AI e servizi simili per comprendere lo schermo, pertanto devono essere cancellate solo se necessario.
  • La semantica personalizzata può essere impostata all'interno della funzione lambda dell'API.
  • L'ordine dei modificatori è importante: questa API cancella tutta la semantica che si trova dopo il punto in cui viene applicata, indipendentemente dalle altre strategie di unione.

Nascondi semantica

In alcuni scenari, non è necessario inviare elementi ai servizi di accessibilità, ad esempio se le informazioni aggiuntive sono ridondanti per l'accessibilità o se sono puramente decorative e non interattive. In questi casi, puoi nascondere gli elementi con l'API hideFromAccessibility.

Negli esempi seguenti sono riportati i componenti che potrebbe essere necessario nascondere: una filigrana ridondante che si estende su un componente e un carattere utilizzato per separare le informazioni in modo decorativo.

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

L'utilizzo di hideFromAccessibility qui garantisce che la filigrana e la decorazione siano nascoste ai servizi di accessibilità, ma mantengano comunque la loro semantica per altri casi d'uso, come i test.

Suddivisione dei casi d'uso

Di seguito è riportato un riepilogo dei casi d'uso per capire come distinguere chiaramente le API precedenti:

  • Quando i contenuti non sono destinati a essere utilizzati dai servizi di accessibilità:
    • Utilizza hideFromAccessibility quando i contenuti sono probabilmente decorativi o ridondanti, ma devono comunque essere testati.
    • Utilizza clearAndSetSemantics{} con una funzione lambda vuota quando la semantica principale e secondaria deve essere cancellata per tutti i servizi.
    • Utilizza clearAndSetSemantics{/*content*/} con i contenuti all'interno della funzione lambda quando la semantica di un componente deve essere impostata manualmente.
  • Quando i contenuti devono essere trattati come un'unica entità e richiedono tutte le informazioni delle entità secondarie per essere completi:
    • Utilizza l'unione dei discendenti semantici.
Tabella con casi d'uso delle API differenziati.
Figura 5. Tabella con casi d'uso delle API differenziati.