Durchlaufreihenfolge festlegen

Standardmäßig wird das Screenreader-Verhalten des Screenreaders für Bedienungshilfen in einer App zum Schreiben in der erwarteten Lesereihenfolge implementiert, also von links nach rechts und dann von oben nach unten. Es gibt jedoch einige Arten von App-Layouts, bei denen der Algorithmus die tatsächliche Lesereihenfolge nicht ohne zusätzliche Hinweise ermitteln kann. In aufrufbasierten Apps können Sie solche Probleme mit den Attributen traversalBefore und traversalAfter beheben. Ab Compose 1.5 stellt Compose eine ebenso flexible API bereit, jedoch mit einem neuen konzeptionellen Modell.

isTraversalGroup und traversalIndex sind semantische Eigenschaften, mit denen Sie die Bedienungshilfen und die TalkBack-Fokusreihenfolge in Szenarien steuern können, in denen der Standardsortieralgorithmus nicht geeignet ist. isTraversalGroup identifiziert semantisch wichtige Gruppen, während traversalIndex die Reihenfolge einzelner Elemente innerhalb dieser Gruppen anpasst. Sie können isTraversalGroup allein oder zur weiteren Anpassung mit traversalIndex verwenden.

Verwenden Sie isTraversalGroup und traversalIndex in Ihrer App, um die Durchlaufreihenfolge des Screenreaders festzulegen.

Elemente mit isTraversalGroup gruppieren

isTraversalGroup ist ein boolesches Attribut, das definiert, ob ein Knoten Semantik eine Durchlaufgruppe ist. Dieser Knotentyp dient dazu, bei der Organisation der untergeordneten Knoten eine Begrenzung oder einen Rahmen zu bilden.

Wenn Sie isTraversalGroup = true für einen Knoten festlegen, werden alle untergeordneten Elemente dieses Knotens besucht, bevor zu anderen Elementen gewechselt wird. Sie können isTraversalGroup auf Knoten festlegen, die nicht für den Screenreader fokussierbar sind, z. B. Spalten, Zeilen oder Felder.

Im folgenden Beispiel wird isTraversalGroup verwendet. Sie gibt vier Textelemente aus. Die beiden Elemente links gehören zu einem CardBox-Unterelement, die beiden Elemente rechts gehören zu einem anderen CardBox-Element:

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

Die Ausgabe des Codes sieht in etwa so aus:

Layout mit zwei Spalten mit Text, wobei in der linken Spalte „Dieser Satz befindet sich in der linken Spalte“ und in der rechten Spalte „Dieser Satz befindet sich in der rechten Spalte“ steht.
Abbildung 1: Ein Layout mit zwei Sätzen (einer in der linken Spalte und einer in der rechten Spalte).

Da keine Semantik festgelegt wurde, durchsucht der Screenreader Elemente standardmäßig von links nach rechts und von oben nach unten. Aufgrund dieser Standardeinstellung liest TalkBack die Satzfragmente in der falschen Reihenfolge vor:

„Dieser Satz ist in“ → „Dieser Satz ist“ → „die linke Spalte“. → „auf der rechten Seite“.

Damit die Fragmente richtig sortiert werden, ändern Sie das ursprüngliche Snippet so, dass isTraversalGroup auf true gesetzt wird:

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

Da isTraversalGroup für jede CardBox explizit festgelegt wird, gelten beim Sortieren der Elemente die CardBox-Grenzen. In diesem Fall wird zuerst die linke CardBox gelesen, gefolgt vom rechten CardBox.

Jetzt liest TalkBack die Satzfragmente in der richtigen Reihenfolge vor:

„Dieser Satz ist in“ → „die linke Spalte“. → „Dieser Satz steht“ → „auf der rechten Seite“.

Durchquerungsreihenfolge weiter anpassen

traversalIndex ist eine Gleitkommazahl, mit der Sie die Durchlaufreihenfolge von TalkBack anpassen können. Wenn das Gruppieren von Elementen nicht ausreicht, damit TalkBack ordnungsgemäß funktioniert, verwenden Sie traversalIndex in Verbindung mit isTraversalGroup, um die Reihenfolge des Screenreaders weiter anzupassen.

Das Attribut traversalIndex hat die folgenden Eigenschaften:

  • Elemente mit niedrigeren traversalIndex-Werten werden zuerst priorisiert.
  • Kann positiv oder negativ sein.
  • Der Standardwert ist 0f.
  • Betrifft nur Knoten, die für Screenreader fokussiert werden können, z. B. Bildschirmelemente wie Text oder Schaltflächen. Wenn Sie beispielsweise nur traversalIndex für eine Spalte festlegen, hat dies keine Auswirkungen, es sei denn, für die Spalte ist auch isTraversalGroup festgelegt.

Das folgende Beispiel zeigt, wie Sie traversalIndex und isTraversalGroup zusammen verwenden können.

Beispiel: Durchlaufendes Zifferblatt

Ein Zifferblatt ist ein gängiges Szenario, in dem die Standarddurchlaufreihenfolge nicht funktioniert. Das Beispiel in diesem Abschnitt ist eine Zeitauswahl, mit der ein Nutzer durch die Zahlen auf einer Uhranzeige blättern und Ziffern für die Stunden- und Minuten-Zeitabschnitte auswählen kann.

Ein Zifferblatt mit einer Zeitauswahl darüber.
Abbildung 2: Ein Bild eines Ziffernblatts.

Im folgenden vereinfachten Snippet gibt es eine CircularLayout, in der 12 Zahlen gezeichnet sind, von denen 12 beginnt und sich im Uhrzeigersinn um den Kreis bewegt:

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

Da das Ziffernblatt mit der Standardreihenfolge von links nach rechts und von oben nach unten nicht logisch gelesen wird, liest TalkBack die Zahlen in falscher Reihenfolge vor. Um dies zu beheben, verwenden Sie den sich erhöhenden Zählerwert, wie im folgenden Snippet gezeigt:

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

Damit die Durchlaufreihenfolge richtig festgelegt werden kann, müssen Sie CircularLayout als Durchlaufgruppe festlegen und isTraversalGroup = true festlegen. Wenn dann jeder Uhrtext in das Layout gezeichnet wird, setze die entsprechende traversalIndex auf den Zählerwert.

Da der Zählerwert kontinuierlich erhöht wird, ist der traversalIndex jedes Taktwerts größer, wenn dem Bildschirm Zahlen hinzugefügt werden. Der Taktwert 0 hat einen traversalIndex von 0 und der Taktwert 1 hat eine traversalIndex von 1. Auf diese Weise wird die Reihenfolge festgelegt, in der TalkBack vorliest. Jetzt werden die Zahlen in CircularLayout in der erwarteten Reihenfolge gelesen.

Da die festgelegten traversalIndexes nur relativ zu anderen Indexen innerhalb derselben Gruppierung sind, wurde die restliche Bildschirmreihenfolge beibehalten. Mit anderen Worten: Die im vorherigen Code-Snippet gezeigten semantischen Änderungen ändern nur die Reihenfolge innerhalb des Ziffernblatts, für das isTraversalGroup = true festgelegt ist.

Auch wenn Sie die CircularLayout's-Semantik auf isTraversalGroup = true festlegen, gelten die Änderungen an traversalIndex weiterhin. Ohne CircularLayout zum Binden werden die zwölf Ziffern des Ziffernblatts jedoch zuletzt gelesen, nachdem alle anderen Elemente auf dem Bildschirm aufgerufen wurden. Das liegt daran, dass alle anderen Elemente die Standard-traversalIndex von 0f haben und die Uhrtextelemente nach allen anderen 0f-Elementen gelesen werden.

Beispiel: Durchlaufreihenfolge für unverankerte Aktionsschaltfläche anpassen

In diesem Beispiel steuern traversalIndex und isTraversalGroup die Durchlaufreihenfolge einer unverankerten Material Design-Aktionsschaltfläche (UAS). Grundlage dieses Beispiels ist das folgende Layout:

Ein Layout mit einer oberen App-Leiste, Beispieltext, einer unverankerten Aktionsschaltfläche und einer unteren App-Leiste.
Abbildung 3: Layout mit einer oberen App-Leiste, Beispieltext, einer unverankerten Aktionsschaltfläche und einer unteren App-Leiste.

Standardmäßig hat das Layout in diesem Beispiel die folgende TalkBack-Reihenfolge:

Obere App-Leiste → Beispieltexte 0 bis 6 → unverankerte Aktionsschaltfläche (FAB) → Untere App-Leiste

Vielleicht möchten Sie, dass sich der Screenreader zuerst auf den FAB konzentriert. So legen Sie eine traversalIndex für ein Material-Element wie einen FAB fest:

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

Wenn in diesem Snippet ein Feld erstellt wird, bei dem isTraversalGroup auf true gesetzt ist und ein traversalIndex für dasselbe Feld festgelegt wird (-1f ist niedriger als der Standardwert von 0f), wird das unverankerte Feld vor allen anderen Bildschirmelementen angezeigt.

Als Nächstes können Sie den unverankerten Kasten und andere Elemente in einem Gerüst platzieren, das ein Material Design-Layout implementiert:

@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 interagiert in folgender Reihenfolge mit den Elementen:

FAB → Obere App-Leiste → Beispieltexte 0 bis 6 → Untere App-Leiste

Weitere Informationen