Semantik beim Schreiben

Eine Komposition beschreibt die UI Ihrer Anwendung und wird durch Ausführen von zusammensetzbaren Funktionen erstellt. Die Zusammensetzung ist eine Baumstruktur, die aus zusammensetzbaren Funktionen besteht, die Ihre UI beschreiben.

Neben der Zusammensetzung befindet sich ein paralleler Baum, der Semantikbaum genannt wird. In dieser Baumstruktur wird deine UI auf eine alternative Weise beschrieben, die für Dienste der Barrierefreiheit im Internet und für das Test-Framework verständlich ist. Bedienungshilfen verwenden den Baum, um die App Nutzern mit bestimmten Anforderungen zu beschreiben. Das Test-Framework verwendet es, um mit Ihrer Anwendung zu interagieren und Aussagen dazu zu machen. Die Semantikstruktur enthält nicht die Informationen zum Zeichnen von zusammensetzbaren Funktionen, jedoch Informationen zur semantischen Bedeutung.

Abbildung 1: Eine typische UI-Hierarchie und ihr Semantikbaum.

Wenn Ihre Anwendung aus zusammensetzbaren Funktionen und Modifikatoren aus der Compose-Foundation und der Materialbibliothek besteht, wird der Semantics-Baum automatisch für Sie gefüllt und generiert. Wenn Sie jedoch benutzerdefinierte zusammensetzbare Funktionen auf unterer Ebene hinzufügen, müssen Sie ihre Semantik manuell angeben. Es kann auch vorkommen, dass Ihre Baumstruktur die Bedeutung der Elemente auf dem Bildschirm nicht richtig oder nicht vollständig darstellt. In diesem Fall können Sie den Baum anpassen.

Sehen Sie sich zum Beispiel diese zusammensetzbare Funktion aus benutzerdefinierten Kalendern an:

Abbildung 2: Ein benutzerdefinierter Kalender, der mit auswählbaren Tageselementen zusammensetzbar ist.

In diesem Beispiel ist der gesamte Kalender als einzelne zusammensetzbare Funktion auf unterer Ebene implementiert, wobei die zusammensetzbare Funktion Layout verwendet und direkt in Canvas gezeichnet wird. Andernfalls erhalten die Bedienungshilfen nicht genügend Informationen über den Inhalt der zusammensetzbaren Funktion und die Auswahl des Nutzers im Kalender. Wenn ein Nutzer beispielsweise auf den Tag mit 17 klickt, erhält das Framework für Barrierefreiheit nur die Beschreibung für das gesamte Kalendersteuerelement. In diesem Fall kündigt die TalkBack-Bedienungshilfe einfach „Kalender“ oder, nur ein wenig besser, „Kalender für April“ an, und der Nutzer würde sich fragen, welcher Tag ausgewählt wurde. Um diese zusammensetzbare Funktion zugänglicher zu machen, müssen Sie semantische Informationen manuell hinzufügen.

Semantikeigenschaften

Alle Knoten im UI-Baum mit einer semantischen Bedeutung haben einen parallelen Knoten im Semantikbaum. Der Knoten in der Semantikstruktur enthält die Eigenschaften, die die Bedeutung der entsprechenden zusammensetzbaren Funktion vermitteln. Die zusammensetzbare Funktion Text enthält beispielsweise das semantische Attribut text, da dies die Bedeutung dieser zusammensetzbaren Funktion ist. Ein Icon enthält eine contentDescription-Eigenschaft (falls vom Entwickler festgelegt), die in Text die Bedeutung von Icon vermittelt. Zusammensetzbare Funktionen und Modifikatoren, die auf der Foundation Library basieren, legen die relevanten Eigenschaften bereits für Sie fest. Optional können Sie die Attribute mit den Modifizierern semantics und clearAndSetSemantics selbst festlegen oder überschreiben. Beispielsweise können Sie einem Knoten benutzerdefinierte Aktionen für Bedienungshilfen hinzufügen, eine alternative Statusbeschreibung für ein ein-/ausschaltbares Element angeben oder angeben, dass eine bestimmte zusammensetzbare Textfunktion als Überschrift betrachtet werden soll.

Zur Visualisierung des Semantikbaums können Sie das Tool Layout Inspector oder die Methode printToLog() in unseren Tests verwenden. Dadurch wird der aktuelle Semantics-Baum in Logcat ausgegeben.

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

Das Ergebnis dieses Tests wäre:

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

Sehen wir uns ein Beispiel an, um zu sehen, wie semantische Attribute verwendet werden, um die Bedeutung einer zusammensetzbaren Funktion zu vermitteln. Sehen wir uns ein Switch an. So sieht das für den Nutzer aus:

Abbildung 3: Ein Schalter, der sich im Status „An“ oder „Aus“ befindet

Die Bedeutung dieses Elements lässt sich z. B. so beschreiben: "Dies ist ein Schalter, bei dem es sich um ein ein-/ausschaltbares Element handelt, das sich derzeit im Status 'An' befindet. Sie können darauf klicken, um damit zu interagieren.“

Genau dafür werden die semantischen Attribute verwendet. Der Semantikknoten dieses Switch-Elements enthält die folgenden Eigenschaften, wie sie im Layout Inspector dargestellt werden:

Abbildung 4: Layout Inspector mit den Semantik-Eigenschaften einer zusammensetzbaren Funktion (Switch).

Das Role gibt an, um welchen Elementtyp es sich handelt. In StateDescription wird beschrieben, wie auf den Status „An“ verwiesen werden soll. Standardmäßig ist dies einfach eine lokalisierte Version des Wortes „An“, aber dies kann je nach Kontext spezifischer werden (z. B. „Aktiviert“). ToggleableState ist der aktuelle Status des Switches. Das Attribut OnClick verweist auf die Methode, die zur Interaktion mit diesem Element verwendet wird. Eine vollständige Liste der semantischen Attribute finden Sie im Objekt SemanticsProperties. Eine vollständige Liste der möglichen Aktionen für Bedienungshilfen finden Sie im Objekt SemanticsActions.

Wenn Sie die semantischen Attribute jeder zusammensetzbaren Funktion in Ihrer App im Auge behalten, eröffnen sich zahlreiche leistungsstarke Möglichkeiten. Hier ein paar Beispiele:

  • TalkBack liest mithilfe der Eigenschaften vor, was auf dem Bildschirm angezeigt wird, und ermöglicht dem Nutzer eine reibungslose Interaktion. Bei unserem Schalter könnte dieser lauten: „Ein; Schalter; zum Wechseln doppeltippen.“. Der Nutzer kann auf das Display doppeltippen, um den Schalter auf „Aus“ zu stellen.
  • Das Test-Framework verwendet die Attribute, um Knoten zu finden, mit ihnen zu interagieren und Assertions zu erstellen. Hier ein Beispieltest für unseren Switch:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Zusammengeführter und nicht zusammengeführter Semantics-Baum

Wie bereits erwähnt, können für jede zusammensetzbare Funktion im UI-Baum null oder mehr Semantik-Attribute festgelegt sein. Wenn für eine zusammensetzbare Funktion keine semantischen Eigenschaften festgelegt wurden, ist sie nicht in die Semantikstruktur aufgenommen. Auf diese Weise enthält der Semantikbaum nur die Knoten, die tatsächlich eine semantische Bedeutung haben. Um die korrekte Bedeutung dessen zu vermitteln, was auf dem Bildschirm angezeigt wird, ist es jedoch auch hilfreich, bestimmte Unterstrukturen von Knoten zusammenzuführen und als einen zu behandeln. Auf diese Weise können wir eine Gruppe von Knoten als Ganzes berücksichtigen, anstatt jeden untergeordneten Knoten einzeln zu behandeln. Als Faustregel gilt, dass jeder Knoten in dieser Struktur bei der Verwendung von Bedienungshilfen ein fokussierbares Element darstellt.

Ein Beispiel für eine solche zusammensetzbare Funktion ist „Button“. Wir möchten die Schaltfläche als einzelnes Element betrachten, auch wenn sie mehrere untergeordnete Knoten enthalten kann:

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

In unserem Semantikbaum werden die Eigenschaften der Nachfolgerelemente der Schaltfläche zusammengeführt und die Schaltfläche wird als einzelne Blattknoten im Baum angezeigt:

Zusammensetzbare Funktionen und Modifikatoren können durch Aufrufen von Modifier.semantics (mergeDescendants = true) {} angeben, dass sie die semantischen Attribute ihrer Nachfolger zusammenführen möchten. Wenn dieses Attribut auf true gesetzt wird, bedeutet das, dass die semantischen Attribute zusammengeführt werden sollen. In unserem Button-Beispiel verwendet die zusammensetzbare Funktion Button intern den clickable-Modifikator, der diesen semantics-Modifikator enthält. Daher werden die untergeordneten Knoten der Schaltfläche zusammengeführt. In der Dokumentation zur Barrierefreiheit finden Sie weitere Informationen dazu, wann Sie das Zusammenführungsverhalten in einer zusammensetzbaren Funktion ändern sollten.

Diese Eigenschaft ist für mehrere Modifikatoren und zusammensetzbare Funktionen in den Bibliotheken „Foundation“ und „Material Compose“ festgelegt. Die Modifikatoren clickable und toggleable führen beispielsweise automatisch ihre Nachfolger zusammen. Außerdem führt die zusammensetzbare Funktion ListItem auch ihre Nachfolger zusammen.

Inspektion der Bäume

Beim Semantics-Baum geht es tatsächlich um zwei verschiedene Bäume. Es gibt einen zusammengeführten Semantics-Baum, der untergeordnete Knoten zusammenführt, wenn mergeDescendants auf true gesetzt ist. Es gibt auch einen nicht zusammengeführten Semantikbaum, in dem die Zusammenführung nicht angewendet wird, aber jeder Knoten intakt bleibt. Bedienungshilfen verwenden den nicht zusammengeführten Baum und wenden eigene Zusammenführungsalgorithmen unter Berücksichtigung des Attributs mergeDescendants an. Das Test-Framework verwendet standardmäßig den zusammengeführten Baum.

Sie können beide Bäume mit der Methode printToLog() untersuchen. Standardmäßig, wie in den vorherigen Beispielen, wird der zusammengeführte Baum protokolliert. Wenn Sie stattdessen die nicht zusammengeführte Struktur ausgeben möchten, legen Sie den Parameter useUnmergedTree des onRoot()-Matchers auf true fest:

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

Mit dem Layout Inspector können Sie sowohl die zusammengeführte als auch die nicht zusammengeführte Semantik-Baumstruktur aufrufen, indem Sie im Ansichtsfilter die bevorzugte Struktur auswählen:

Abbildung 5: Layout Inspector-Ansichtsoptionen, mit denen Sie sowohl den zusammengeführten als auch den noch nicht zusammengeführten Semantics-Baum anzeigen lassen können

Der Layout Inspector zeigt für jeden Knoten in der Baumstruktur sowohl die zusammengeführte als auch die auf diesem Knoten festgelegte Semantik im Eigenschaftenbereich an:

Standardmäßig verwenden Abgleicher im Test-Framework die zusammengeführte Semantik-Struktur. Aus diesem Grund können Sie mit einer Schaltfläche interagieren, indem Sie den darin angezeigten Text abgleichen:

composeTestRule.onNodeWithText("Like").performClick()

Sie können dieses Verhalten überschreiben, indem Sie den useUnmergedTree-Parameter des Matchers auf true setzen, wie zuvor beim onRoot-Matcher.

Verhalten bei der Zusammenführung

Wie genau wird bei einer zusammensetzbaren Funktion angegeben, dass ihre Nachfolger zusammengeführt werden sollen?

Für jedes semantische Attribut gibt es eine definierte Zusammenführungsstrategie. Mit dem Attribut ContentDescription werden beispielsweise alle untergeordneten ContentDescription-Werte einer Liste hinzugefügt. Die Zusammenführungsstrategie eines semantischen Attributs wird in der mergePolicy-Implementierung in SemanticsProperties.kt geprüft. Attribute können entweder immer den übergeordneten oder untergeordneten Wert auswählen, die Werte in einer Liste oder einem String zusammenführen, das Zusammenführen überhaupt nicht zulassen und stattdessen eine Ausnahme auslösen oder eine andere benutzerdefinierte Zusammenführungsstrategie verwenden.

Wichtiger Hinweis: Nachfolgerelemente, für die mergeDescendants = true festgelegt wurde, werden nicht in die Zusammenführung einbezogen. Sehen wir uns ein Beispiel an:

Abbildung 6: Listeneintrag mit Bild, Text und Lesezeichensymbol.

Hier haben wir einen anklickbaren Listeneintrag. Wenn der Nutzer auf die Zeile klickt, gelangt er zur Seite mit den Artikeldetails, auf der er den Artikel lesen kann. Im Listeneintrag befindet sich eine Schaltfläche, über die Sie ein Lesezeichen für diesen Artikel erstellen können. In diesem Fall haben wir ein verschachteltes anklickbares Element, sodass die Schaltfläche separat in der zusammengeführten Struktur angezeigt wird. Der Rest des Inhalts in der Zeile wird zusammengeführt:

Abbildung 7: Die zusammengeführte Struktur enthält mehrere Texte in einer Liste innerhalb des Zeilenknotens. Die nicht zusammengeführte Struktur enthält separate Knoten für jede zusammensetzbare Textfunktion.

Semantikbaum anpassen

Wie bereits erwähnt, können Sie bestimmte semantische Attribute überschreiben oder löschen oder das Zusammenführungsverhalten der Struktur ändern. Das ist besonders relevant, wenn Sie Ihre eigenen benutzerdefinierten Komponenten erstellen. Ohne die richtigen Eigenschaften und das Zusammenführungsverhalten ist der Zugriff auf Ihre Anwendung möglicherweise nicht möglich und Tests verhalten sich möglicherweise anders als erwartet. Weitere Informationen zu häufigen Anwendungsfällen, bei denen Sie die Semantikstruktur anpassen sollten, finden Sie in der Dokumentation zur Barrierefreiheit. Weitere Informationen zu Tests finden Sie im Testleitfaden.