Ordine di attraversamento del controllo

Per impostazione predefinita, il comportamento dello screen reader di accessibilità in un'app Compose viene implementato nell'ordine di lettura previsto, che di solito è da sinistra a destra e poi dall'alto verso il basso. Tuttavia, per alcuni tipi di layout di app l'algoritmo non riesce a determinare l'ordine di lettura effettivo senza suggerimenti aggiuntivi. Nelle app basate sulle visualizzazioni, puoi risolvere questi problemi utilizzando le proprietà traversalBefore e traversalAfter. A partire da Compose 1.5, Compose fornisce un'API altrettanto flessibile, ma con un nuovo modello concettuale.

isTraversalGroup e traversalIndex sono proprietà semantiche che consentono di controllare l'accessibilità e l'ordine di impostazione dello stato attivo di TalkBack negli scenari in cui l'algoritmo di ordinamento predefinito non è appropriato. isTraversalGroup identifica i gruppi semanticamente importanti, mentre traversalIndex regola l'ordine dei singoli elementi all'interno di questi gruppi. Puoi utilizzare isTraversalGroup da solo o con traversalIndex per un'ulteriore personalizzazione.

Utilizza isTraversalGroup e traversalIndex nella tua app per controllare l'ordine di attraversamento dello screen reader.

Raggruppa elementi con isTraversalGroup

isTraversalGroup è una proprietà booleana che definisce se un nodo semantica è un gruppo di attraversamento. Questo tipo di nodo ha la funzione di servire come confine o confine nell'organizzazione degli elementi figlio del nodo.

L'impostazione di isTraversalGroup = true su un nodo fa sì che tutti i nodi secondari vengano visitati prima di essere spostati ad altri elementi. Puoi impostare isTraversalGroup su nodi non attivabili per screen reader, come Colonne, Righe o Caselle.

L'esempio seguente utilizza isTraversalGroup. Emette quattro elementi di testo. I due elementi a sinistra appartengono a un elemento CardBox, mentre i due elementi a destra appartengono a un altro elemento CardBox:

// CardBox() function takes in top and bottom sample text.
@Composable
fun CardBox(
    topSampleText: String,
    bottomSampleText: String,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        Column {
            Text(topSampleText)
            Text(bottomSampleText)
        }
    }
}

@Composable
fun TraversalGroupDemo() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is "
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
            topSampleText1,
            bottomSampleText1
        )
        CardBox(
            topSampleText2,
            bottomSampleText2
        )
    }
}

Il codice produce un output simile al seguente:

Layout con due colonne di testo, con la colonna di sinistra "Questa frase è nella colonna di sinistra" e la colonna di destra "Questa frase è sulla destra".
Figura 1. Un layout con due frasi (una nella colonna di sinistra e una nella colonna di destra).

Poiché non è stata impostata alcuna semantica, il comportamento predefinito dello screen reader è quello di attraversare gli elementi da sinistra a destra e dall'alto verso il basso. Per via di questa impostazione predefinita, TalkBack legge i frammenti di frase nell'ordine sbagliato:

"Questa frase è in" → "Questa frase è" → "colonna sinistra". → "a destra".

Per ordinare correttamente i frammenti, modifica lo snippet originale in modo da impostare isTraversalGroup su true:

@Composable
fun TraversalGroupDemo2() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is"
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
//      1,
            topSampleText1,
            bottomSampleText1,
            Modifier.semantics { isTraversalGroup = true }
        )
        CardBox(
//      2,
            topSampleText2,
            bottomSampleText2,
            Modifier.semantics { isTraversalGroup = true }
        )
    }
}

Poiché isTraversalGroup è impostato specificamente su ogni CardBox, i confini di CardBox vengono applicati quando si ordinano gli elementi. In questo caso, viene letto per primo il valore CardBox sinistro, seguito dal CardBox destro.

Ora TalkBack legge i frammenti di frase nell'ordine corretto:

"Questa frase è in" → "la colonna di sinistra". → "Questa frase è" → "a destra".

Personalizza ulteriormente l'ordine di attraversamento

traversalIndex è una proprietà float che consente di personalizzare l'ordine di attraversamento di TalkBack. Se il raggruppamento degli elementi non è sufficiente per il corretto funzionamento di TalkBack, usa traversalIndex insieme a isTraversalGroup per personalizzare ulteriormente l'ordinamento dello screen reader.

La proprietà traversalIndex ha le seguenti caratteristiche:

  • Gli elementi con valori traversalIndex più bassi hanno la priorità per primo.
  • Può essere positiva o negativa.
  • Il valore predefinito è 0f.
  • Interessa solo i nodi attivabili per lo screen reader, ad esempio gli elementi sullo schermo come testo o pulsanti. Ad esempio, l'impostazione di solo traversalIndex in una colonna non avrebbe alcun effetto, a meno che anche per la colonna sia impostato isTraversalGroup.

L'esempio seguente mostra come utilizzare traversalIndex e isTraversalGroup insieme.

Esempio: Esplora quadrante orologio

Un quadrante orologio è uno scenario comune in cui l'ordinamento di attraversamento standard non funziona. L'esempio in questa sezione è un selettore dell'ora, in cui un utente può scorrere i numeri su un quadrante orologio e selezionare le cifre per le fasce orarie e minuti.

Un quadrante orologio con un selettore dell'ora sopra.
Figura 2. L'immagine di un quadrante orologio.

Nel seguente snippet semplificato, è presente un elemento CircularLayout in cui vengono tracciati 12 numeri, a partire dal numero 12 che si sposta in senso orario intorno al cerchio:

@Composable
fun ClockFaceDemo() {
    CircularLayout {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier) {
        Text((if (value == 0) 12 else value).toString())
    }
}

Poiché il quadrante orologio non viene letto logicamente con l'ordinamento predefinito da sinistra a destra e dall'alto verso il basso, TalkBack legge i numeri in ordine sbagliato. Per risolvere questo problema, utilizza il valore del contatore incrementale, come mostrato nello snippet seguente:

@Composable
fun ClockFaceDemo() {
    CircularLayout(Modifier.semantics { isTraversalGroup = true }) {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier.semantics { this.traversalIndex = value.toFloat() }) {
        Text((if (value == 0) 12 else value).toString())
    }
}

Per impostare correttamente l'ordine di attraversamento, devi prima rendere CircularLayout un gruppo di attraversamento e impostare isTraversalGroup = true. Quindi, man mano che il testo di ogni orologio viene disegnato sul layout, imposta il valore traversalIndex corrispondente sul valore del contatore.

Poiché il valore del contatore aumenta continuamente, il valore traversalIndex di ogni valore dell'orologio è maggiore man mano che vengono aggiunti numeri alla schermata: il valore dell'orologio 0 ha un traversalIndex pari a 0 e il valore dell'orologio 1 ha un valore traversalIndex pari a 1. In questo modo viene impostato l'ordine in cui TalkBack legge le letture. Ora i numeri all'interno di CircularLayout vengono letti nell'ordine previsto.

Poiché i valori traversalIndexes che sono stati impostati sono relativi solo ad altri indici nello stesso raggruppamento, il resto dell'ordine delle schermate è stato conservato. In altre parole, i cambiamenti semantici mostrati nello snippet di codice precedente modificano solo l'ordine all'interno del quadrante orologio che ha impostato isTraversalGroup = true.

Tieni presente che, senza impostare la semantica di CircularLayout's su isTraversalGroup = true, le modifiche di traversalIndex vengono comunque applicate. Tuttavia, senza l'CircularLayout per associarli, le dodici cifre del quadrante orologio vengono lette per ultime, dopo che tutti gli altri elementi sullo schermo sono stati visitati. Questo accade perché tutti gli altri elementi hanno un valore predefinito di traversalIndex di 0f e gli elementi di testo dell'orologio vengono letti dopo tutti gli altri elementi 0f.

Esempio: personalizzare l'ordine di attraversamento per il pulsante di azione mobile

In questo esempio, traversalIndex e isTraversalGroup controllano l'ordine di attraversamento di un pulsante di azione mobile (FAB) di Material Design. La base di questo esempio è il seguente layout:

Un layout con una barra delle app superiore, testo di esempio, un pulsante di azione mobile e una barra dell'app in basso.
Figura 3. Layout con barra delle app superiore, testo di esempio, pulsante di azione mobile e barra dell'app in basso.

Per impostazione predefinita, il layout di questo esempio ha il seguente ordine di TalkBack:

Barra dell'app in alto → Testi di esempio da 0 a 6 → Pulsante di azione mobile (FAB) → Barra dell'app in basso

È consigliabile che lo screen reader si concentri prima sul FAB. Per impostare traversalIndex su un elemento Material come un FAB:

@Composable
fun FloatingBox() {
    Box(modifier = Modifier.semantics { isTraversalGroup = true; traversalIndex = -1f }) {
        FloatingActionButton(onClick = {}) {
            Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
        }
    }
}

In questo snippet, se crei una casella con isTraversalGroup impostato su true e l'impostazione di traversalIndex per la stessa casella (-1f è inferiore al valore predefinito di 0f), la casella mobile viene visualizzata prima di tutti gli altri elementi sullo schermo.

Quindi, puoi posizionare la scatola mobile e altri elementi in un'impalcatura, in modo da implementare un layout di Material Design:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColumnWithFABFirstDemo() {
    Scaffold(
        topBar = { TopAppBar(title = { Text("Top App Bar") }) },
        floatingActionButtonPosition = FabPosition.End,
        floatingActionButton = { FloatingBox() },
        content = { padding -> ContentColumn(padding = padding) },
        bottomBar = { BottomAppBar { Text("Bottom App Bar") } }
    )
}

TalkBack interagisce con gli elementi nel seguente ordine:

FAB → Barra dell'app superiore → Testi di esempio da 0 a 6 → Barra dell'app in basso

Risorse aggiuntive