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 als Semantikbaum bezeichnet wird. Dieser Baum beschreibt Ihre UI auf eine alternative Art und Weise, die für Bedienungshilfen-Dienste und das Testing-Framework verständlich ist. Bedienungshilfen verwenden den Baum, um die App für Nutzer mit bestimmten Anforderungen zu beschreiben. Das Test-Framework verwendet den Baum, 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.

Eine typische UI-Hierarchie und ihre Semantikstruktur
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:

Ein benutzerdefinierter Kalender, der mit auswählbaren Tageselementen zusammensetzbar ist
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. Wenn Sie nichts weiter tun, 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 sagt die TalkBack-Bedienungshilfe „Kalender“ oder, nur geringfügig 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-Bibliothek aufbauen, legen die relevanten Eigenschaften bereits für Sie fest. Optional können Sie die Attribute mit den Modifikatoren semantics und clearAndSetSemantics 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.

Sie können den Semantics-Baum mit dem Tool Layout Inspector oder der Methode printToLog() in Tests visualisieren. Damit wird der aktuelle Semantik-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 Sie sich an, wie semantische Attribute die Bedeutung einer zusammensetzbaren Funktion vermitteln. Beispiel: Switch. So sieht das für den Nutzer aus:

Abbildung 3. Ein Schalter im Status „An“ oder „Aus“

Um die Bedeutung dieses Elements zu beschreiben, könnten Sie Folgendes sagen: "Dies ist ein Schalter, bei dem es sich um ein ein-/ausschaltbares Element im Status 'An' handelt. Sie können darauf klicken, um mit ihr 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:

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

Role gibt den Elementtyp an. Unter StateDescription wird beschrieben, wie auf den Status „An“ verwiesen werden soll. Standardmäßig ist dies eine lokalisierte Version des Wortes „An“, aber dies kann je nach Kontext spezifischer werden (z. B. „Aktiviert“). ToggleableState ist der aktuelle Status des Switches. Die Eigenschaft 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 der zusammensetzbaren Funktion „Wechsel“ könnte TalkBack Folgendes sagen: „Ein; Schalter; zum Umschalten doppeltippen.“. Der Nutzer kann auf das Display doppeltippen, um den Schalter zu deaktivieren.
  • Das Test-Framework verwendet die Attribute, um Knoten zu finden, mit ihnen zu interagieren und Assertions zu erstellen. Hier ein Beispieltest für den 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 Attribute 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 Sie eine Gruppe von Knoten als Ganzes bedenken, anstatt sich mit jedem untergeordneten Knoten einzeln befassen zu müssen. 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. Sie können eine Schaltfläche als einzelnes Element begründen, 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 der Semantikstruktur werden die Eigenschaften der Nachfolgerelemente der Schaltfläche zusammengeführt und die Schaltfläche wird als einzelne Blattknoten im Baum angezeigt:

Zusammengeführte einblatt-Semantikdarstellung
Abbildung 5. Zusammengeführte Einzelblatt-Semantikdarstellung.

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. Im Beispiel Button 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 erfahren Sie, 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.

Bäume inspizieren

Der Semantikbaum besteht eigentlich aus zwei unterschiedlichen Bäumen. Es gibt einen zusammengeführten Semantikbaum, der untergeordnete Knoten zusammenführt, wenn mergeDescendants auf true gesetzt ist. Es gibt auch einen nicht zusammengeführten Semantics-Baum, in dem die Zusammenführung nicht angewendet wird, aber jeder Knoten intakt bleibt. Bedienungshilfen verwenden die noch nicht zusammengeführte Struktur 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 den zusammengeführten als auch den nicht zusammengeführten Semantikbaum aufrufen. Wählen Sie dazu im Ansichtsfilter den bevorzugten Baum aus:

Layout Inspector-Ansichtsoptionen, mit denen Sie sowohl die zusammengeführte als auch die noch nicht zusammengeführte Semantikstruktur anzeigen können
Abbildung 6. Layout Inspector-Ansichtsoptionen, mit denen Sie sowohl die zusammengeführte als auch die noch nicht zusammengeführte Semantikstruktur anzeigen 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:

Semantikeigenschaften zusammengeführt und festgelegt
Abbildung 7. Semantikeigenschaften zusammengeführt und festgelegt.

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

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

Sie können dieses Verhalten überschreiben, indem Sie den useUnmergedTree-Parameter der Abgleichausdrücke auf true setzen, wie beim onRoot-Matcher.

Zusammenführungsverhalten

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. Das Attribut ContentDescription fügt beispielsweise alle untergeordneten ContentDescription-Werte in eine Liste ein. Prüfen Sie die Zusammenführungsstrategie eines semantischen Attributs, indem Sie dessen mergePolicy-Implementierung in SemanticsProperties.kt prüfen. Attribute können den übergeordneten oder untergeordneten Wert übernehmen, die Werte in einer Liste oder einem String zusammenführen, das Zusammenführen überhaupt nicht zulassen und stattdessen eine Ausnahme ausgeben, oder eine andere benutzerdefinierte Zusammenführungsstrategie verwenden.

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

Listeneintrag mit Bild, Text und Lesezeichensymbol
Abbildung 8. Listeneintrag mit Bild, Text und Lesezeichensymbol.

Hier ist ein anklickbarer Listeneintrag. Wenn der Nutzer auf die Zeile klickt, gelangt er zur Seite mit den Artikeldetails, auf der er den Artikel lesen kann. Im Listenelement befindet sich eine Schaltfläche, über die Sie den Artikel als Lesezeichen speichern können. Sie bilden 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:

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.
Abbildung 9. 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. Dies ist besonders relevant, wenn Sie Ihre eigenen benutzerdefinierten Komponenten erstellen. Ohne die richtigen Eigenschaften und das Zusammenführungsverhalten ist Ihre Anwendung möglicherweise nicht zugänglich 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 findest du im Testleitfaden.

Weitere Informationen