Unioni e sgomberi

Quando i servizi di accessibilità navigano tra gli elementi sullo schermo, è importante che questi elementi siano raggruppati, separati o addirittura nascosti con la granularità corretta. Quando ogni singolo componente composable di basso livello nella schermata è evidenziato in modo indipendente, gli utenti devono interagire molto per spostarsi nella schermata. Se gli elementi vengono uniti in modo troppo aggressivo, gli utenti potrebbero non capire quali elementi appartengono logicamente l'uno all'altro. Se sullo schermo sono presenti elementi puramente decorativi, questi potrebbero essere nascosti ai servizi di accessibilità. In questi casi, puoi utilizzare le API Compose per unire, cancellare e nascondere la semantica.

Semantica dell'unione

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

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

Ad esempio, immagina un composable che mostri l'avatar di un utente, il suo nome e alcune informazioni aggiuntive:

Un gruppo di elementi dell'interfaccia utente che include il nome di un utente. Il nome è selezionato.
Figura 1. Un gruppo di elementi dell'interfaccia utente che include il nome di un utente. Il nome è selezionato.

Puoi consentire a Compose di unire questi elementi utilizzando il parametro mergeDescendants nel modificatore della 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")
        }
    }
}

Ora i servizi di accessibilità si concentrano sull'intero contenitore contemporaneamente e ne uniscono i contenuti:

Un gruppo di elementi dell'interfaccia utente che include il nome di un utente. Tutti gli elementi vengono selezionati insieme.
Figura 2. Un gruppo di elementi dell'interfaccia utente che include 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 diretti a un elenco. Puoi controllare la strategia di unione di una proprietà di semantica controllandone l'implementazione mergePolicy in SemanticsProperties.kt. Le proprietà possono assumere il valore principale o secondario, unire i valori in un elenco o una stringa, non consentire l'unione e generare un'eccezione oppure qualsiasi altra strategia di unione personalizzata.

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

Elemento dell'elenco con immagine, testo e icona segnalibro
Figura 3. Elemento dell'elenco con immagine, testo e icona 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, si apre l'articolo. Al suo 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 Riga. L'albero non unito contiene nodi separati per ogni componibile di testo.
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 Textcomposable
.

Per impostazione predefinita, alcuni composabili non vengono uniti automaticamente in un elemento principale. Un elemento primario non può unire i propri elementi secondari quando anche questi ultimi vengono uniti, impostando esplicitamente mergeDescendants = true o essendo componenti che si uniscono, come pulsanti o elementi cliccabili. Sapere in che modo determinate API si fondono o si oppongono alla fusione può aiutarti a eseguire il debug di alcuni comportamenti potenzialmente imprevisti.

Utilizza l'unione quando gli elementi secondari costituiscono un gruppo logico e sensato sotto l'elemento principale. Tuttavia, se gli elementi nidificati secondari richiedono la regolazione o la rimozione manuale della loro semantica, altre API potrebbero soddisfare meglio le tue esigenze (ad esempio, clearAndSetSemantics).

Cancella e imposta la semantica

Se le informazioni semantiche devono essere completamente cancellate o sovrascritte, è consigliabile utilizzare una potente API come clearAndSetSemantics.

Quando un componente deve cancellare la propria semantica e quella dei suoi discendenti, utilizza questa API con un lambda vuoto. Quando la semantica deve essere sovrascritta, includi i nuovi contenuti all'interno della funzione lambda.

Tieni presente che, quando viene eseguita l'eliminazione con un lambda vuoto, la semantica eliminata non viene inviata a nessun consumatore 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 tutte le semantiche precedenti dell'elemento e dei suoi discendenti.

Di seguito è riportato un esempio di componente di pulsante 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 pulsante di attivazione/disattivazione personalizzato, devi aggiungere la funzionalità di attivazione/disattivazione, nonché le semantiche 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".

Se elimini la semantica originale e ne imposti di nuove più descrittive, i servizi di accessibilità ora possono 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 usarla con parsimonia.
    • Le informazioni semantiche possono essere potenzialmente utilizzate da agenti IA e servizi simili per comprendere la schermata e pertanto devono essere cancellate solo se necessario.
  • La semantica personalizzata può essere impostata all'interno dell'API lambda.
  • L'ordine dei modificatori è importante: questa API cancella tutta la semantica successiva al punto in cui viene applicata, indipendentemente da altre strategie di unione.

Nascondere la semantica

In alcuni scenari, gli elementi non devono essere inviati ai servizi di accessibilità, ad esempio perché le informazioni aggiuntive sono ridondanti per l'accessibilità o sono puramente decorative e non interattive. In questi casi, puoi nascondere gli elementi con l'API hideFromAccessibility.

Nei seguenti esempi sono riportati i componenti che potrebbero dover essere nascosti: una filigrana ridondante che si estende su un componente e un carattere utilizzato per separare decorativamente le informazioni.

@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 la loro semantica per altri casi d'uso, come i test.

Analisi 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 all'utilizzo da parte dei servizi di accessibilità:
    • Utilizza hideFromAccessibility quando i contenuti sono eventualmente decorativi o ridondanti, ma devono comunque essere testati.
    • Utilizza clearAndSetSemantics{} con una lambda vuota quando è necessario cancellare la semantica di elementi principali e secondari per tutti i servizi.
    • Utilizza clearAndSetSemantics{/*content*/} con i contenuti all'interno della funzione lambda quando è necessario impostare manualmente la semantica di un componente.
  • Quando i contenuti devono essere trattati come un'unica entità e richiedono che tutte le informazioni delle relative entità secondarie siano complete:
    • Utilizza i discendenti semantici dell'unione.
Tabella con casi d'uso delle API differenziati.
Figura 5. Tabella con casi d'uso differenziati delle API.