Semantyka

Oprócz podstawowych informacji, które zawiera komponent, takich jak ciąg tekstowy komponentu Text, przydatne mogą być dodatkowe informacje o elementach interfejsu.

Informacje o znaczeniu i roli komponentu w Compose to separacja, która umożliwia przekazywanie dodatkowych informacji o elementach kompozytowych usługom takim jak ułatwienia dostępu, autouzupełnianie i testowanie. Na przykład ikona aparatu może być tylko obrazem, ale jej znaczenie semantyczne może być „Zrób zdjęcie”.

Łącząc odpowiednią semantykę z odpowiednimi interfejsami Compose, możesz przekazać usługom ułatwień dostępu jak najwięcej informacji o swoim komponencie. Usługi te mogą następnie zdecydować, jak przedstawić go użytkownikowi.

Interfejsy API Material i Compose UI oraz Foundation mają wbudowaną semantykę, która odpowiada ich roli i funkcji, ale możesz też zmodyfikować tę semantykę w przypadku istniejących interfejsów API lub ustawić nową w przypadku komponentów niestandardowych zgodnie ze swoimi wymaganiami.

Właściwości semantyczne

Właściwości semantyczne przekazują znaczenie odpowiedniego komponentu. Na przykład kompozyt Text zawiera atrybuty semantyczne text, ponieważ to znaczenie tego kompozytu. Element Icon zawiera właściwość contentDescription (jeśli została ustawiona przez programistę), która w tekstowej formie przekazuje znaczenie ikony.

Zastanów się, jak właściwości semantyczne przekazują znaczenie kompozytowego. Rozważ Switch. Tak to wygląda dla użytkownika:

Rysunek 1. Switch w stanie „Włączone” i „Wyłączone”.

Aby opisać znaczenie tego elementu, możesz powiedzieć: "To jest przełącznik, który jest przełącznikiem przełączalnym w stanie „Włączono”. Możesz kliknąć ten przycisk, aby z nim wchodzić w interakcje”.

Dokładnie do tego służą właściwości semantyczne. Węzeł semantyczny tego elementu przełącznika zawiera te właściwości, jak pokazano w inspektorze układu:

Narzędzie Layout Inspector pokazujące właściwości semantyczne elementu kompozycyjnego Switch
Rysunek 2. Panel Inspekcja układu z właściwościami semantyki elementu kompozycyjnego Switch
.

Role wskazuje typ elementu. Właściwość StateDescription opisuje, jak należy odwoływać się do stanu „Wł.”. Domyślnie jest to zlokalizowana wersja słowa „Wł.”, ale w zależności od kontekstu można użyć bardziej szczegółowego słowa (np. „Włączone”). ToggleableState to bieżący stan przełącznika. Właściwość OnClick odwołuje się do metody używanej do interakcji z tym elementem.

Śledzenie właściwości semantycznych każdego komponentu w aplikacji otwiera wiele możliwości:

  • Usługi ułatwień dostępu używają właściwości do wyświetlania interfejsu użytkownika na ekranie i ułatwiania użytkownikom interakcji z nim. W przypadku przełącznika usługa TalkBack może ogłosić: „Włączone; przełącznik; kliknij dwukrotnie, aby przełączyć”. Użytkownik może dwukrotnie dotknąć ekranu, aby wyłączyć przełącznik.
  • Platforma testowa używa właściwości do znajdowania węzłów, interakcji z nimi i dokonywania stwierdzeń:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

Elementy składane i modyfikatory utworzone na podstawie biblioteki podstawowej w komponowaniu są już domyślnie ustawione w odpowiednich właściwościach. Opcjonalnie możesz zmienić te właściwości ręcznie, aby poprawić obsługę ułatwień dostępu w przypadku określonych zastosowań, lub zmienić strategię łączenia lub czyszczenia komponentów.

Aby zasygnalizować usługi ułatwień dostępu o konkretnym typie treści komponentu, możesz zastosować różne rodzaje semantyki. Te dodatki będą wspierać główne informacje semantyczne i pomagać usługom ułatwień w dostosowywaniu sposobu wyświetlania, ogłaszania i interakcji z elementem.

Pełną listę właściwości semantycznych znajdziesz w obiekcie SemanticsProperties. Pełną listę możliwych działań dotyczących ułatwień dostępu znajdziesz w obiekcie SemanticsActions.

Nagłówki

Aplikacje często zawierają ekrany z dużą ilością tekstu, takie jak długie artykuły czy strony z wiadomościami, które są zwykle podzielone na różne podsekcje z tytułami:

Post na blogu z tekstem artykułu w sekcji, którą można przewijać.
Rysunek 3. Post na blogu z tekstem artykułu w ruchomym kontenerze.

Użytkownicy z potrzebami dotyczącymi ułatwień dostępu mogą mieć trudności z łatwym poruszaniem się po takim ekranie. Aby ułatwić nawigację, niektóre usługi ułatwień dostępu umożliwiają łatwiejszą nawigację bezpośrednio między sekcjami lub nagłówkami. Aby to umożliwić, określ, że komponent jest heading, definiując jego właściwość semantyki:

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

Alerty i wyskakujące okienka

Jeśli komponent to alert lub wyskakujące okienko, na przykład Snackbar, możesz przekazać usługom ułatwień dostępu informacje o tym, że nowa struktura lub aktualizacje treści mogą być przekazywane użytkownikom.

Komponenty podobne do alertów można oznaczyć atrybutem semantycznym liveRegion. Dzięki temu usługi ułatwień dostępu mogą automatycznie powiadamiać użytkownika o zmianach w tym komponencie lub jego elementach podrzędnych:

PopupAlert(
    message = "You have a new message",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

Tagu liveRegionMode.Polite należy używać w większości przypadków, gdy uwagę użytkowników należy przykuć tylko na chwilę, aby mogli zobaczyć ostrzeżenia lub ważne treści na ekranie.

Aby uniknąć zakłóceń w opinii, należy oszczędnie używać liveRegion.Assertive. Powinien być używany w sytuacjach, w których ważne jest poinformowanie użytkowników o treściach o charakterze czasowym:

PopupAlert(
    message = "Emergency alert incoming",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Assertive
    }
)

Regionów na żywo nie należy używać w przypadku treści, które są często aktualizowane, np. zegarów odliczających czas, aby nie przytłaczać użytkowników ciągłymi informacjami zwrotnymi.

Komponenty podobne do okien

Komponenty niestandardowe przypominające okna, takie jak ModalBottomSheet, wymagają dodatkowych sygnałów, aby odróżnić je od otaczających treści. W tym celu możesz użyć semantyki paneTitle, aby wszelkie zmiany w oknie lub panelu były odpowiednio przedstawiane przez usługi ułatwień dostępu wraz z głównymi informacjami semantycznymi:

ShareSheet(
    message = "Choose how to share this photo",
    modifier = Modifier
        .fillMaxWidth()
        .align(Alignment.TopCenter)
        .semantics { paneTitle = "New bottom sheet" }
)

Informacje na ten temat znajdziesz w artykule Jak Material 3 używa paneTitle w komponentach.

Komponenty błędu

W przypadku innych typów treści, takich jak komponenty podobne do błędów, warto uzupełnić główne informacje semantyczne o informacje dla użytkowników z potrzebami w zakresie ułatwień dostępu. Podczas definiowania stanów błędów możesz poinformować usługi ułatwień dostępu o semantyce error i udostępnić rozszerzone komunikaty o błędach.

W tym przykładzie TalkBack odczytuje najpierw główną informację o błędzie, a potem dodatkowe, rozszerzone informacje:

Error(
    errorText = "Fields cannot be empty",
    modifier = Modifier
        .semantics {
            error("Please add both email and password")
        }
)

Komponenty śledzenia postępów

W przypadku komponentów niestandardowych, które śledzą postęp, możesz chcieć powiadomić użytkowników o zmianach postępu, w tym o bieżącej wartości postępu, jego zakresie i wielkości kroku. Możesz to zrobić za pomocą semantyki progressBarRangeInfo. Dzięki temu usługi ułatwień dostępu będą wiedzieć o zmianach postępu i będą mogły odpowiednio aktualizować użytkowników. Różne technologie wspomagające mogą też mieć unikalne sposoby sugerowania zwiększania i zmniejszania progresji.

ProgressInfoBar(
    modifier = Modifier
        .semantics {
            progressBarRangeInfo =
                ProgressBarRangeInfo(
                    current = progress,
                    range = 0F..1F
                )
        }
)

Informacje o liście i elementach

W przypadku niestandardowych list i tablic z dużą liczbą elementów może być przydatne, aby usługi ułatwień dostępu otrzymywały też bardziej szczegółowe informacje, np. łączną liczbę elementów i indeksów.

Dzięki semantycznym elementom collectionInfocollectionItemInfo na liście usługa dostępności może poinformować użytkowników, na którym miejscu w kolekcji znajduje się element, o którym mowa. Dodatkowo usługa może wyświetlić informacje semantyczne w formie tekstu:

MilkyWayList(
    modifier = Modifier
        .semantics {
            collectionInfo = CollectionInfo(
                rowCount = milkyWay.count(),
                columnCount = 1
            )
        }
) {
    milkyWay.forEachIndexed { index, text ->
        Text(
            text = text,
            modifier = Modifier.semantics {
                collectionItemInfo =
                    CollectionItemInfo(index, 0, 0, 0)
            }
        )
    }
}

Opis stanu

Element kompozycyjny może zdefiniować stateDescription dla semantyki, której framework Androida używa do odczytu stanu elementu kompozycyjnego. Na przykład kompozyt z przełącznikiem może być w stanie „zaznaczony” lub „niezaznaczony”. W niektórych przypadkach warto zastąpić domyślne etykiety stanu, których używa aplikacja Compose. Aby to zrobić, przed zdefiniowaniem kompozytowalności jako przełącznika należy wyraźnie określić etykiety opisu stanu:

@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
    val stateSubscribed = stringResource(R.string.subscribed)
    val stateNotSubscribed = stringResource(R.string.not_subscribed)
    Row(
        modifier = Modifier
            .semantics {
                // Set any explicit semantic properties
                stateDescription = if (selected) stateSubscribed else stateNotSubscribed
            }
            .toggleable(
                value = selected,
                onValueChange = { onToggle() }
            )
    ) {
        /* ... */
    }
}

Działania niestandardowe

Działania niestandardowe mogą być używane do bardziej złożonych gestów na ekranie dotykowym, takich jak przesunięcie, aby zamknąć, czy przeciąganie i upuszczanie, ponieważ mogą one stanowić wyzwanie dla użytkowników z ograniczeniami ruchowymi lub innymi niepełnosprawnościami.

Aby ułatwić gestyk Swipe to dismiss, możesz połączyć go z działaniem niestandardowym, przekazując do niego działanie i etykietę:

SwipeToDismissBox(
    modifier = Modifier.semantics {
        // Represents the swipe to dismiss for accessibility
        customActions = listOf(
            CustomAccessibilityAction(
                label = "Remove article from list",
                action = {
                    removeArticle()
                    true
                }
            )
        )
    },
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {}
) {
    ArticleListItem()
}

Usługa ułatwień dostępu, taka jak TalkBack, wyróżnia komponent i wskazuje, że w menu są dostępne inne czynności, co oznacza, że można przesunąć palcem, aby zamknąć:

Wizualizacja menu czynności TalkBack
Rysunek 4. Wizualizacja menu czynności TalkBack

Innym zastosowaniem działań niestandardowych są długie listy elementów z większą liczbą dostępnych działań, ponieważ przeglądanie każdego działania osobno w przypadku każdego elementu może być żmudne:

=Wizualizacja nawigacji Switch Access na ekranie
Rysunek 5. Wizualizacja nawigacji Switch Access na ekranie.

Aby ulepszyć nawigację (co jest szczególnie przydatne w przypadku technologii wspomagających opartych na interakcjach, takich jak Switch Access czy Voice Access), możesz użyć niestandardowych działań w kontenerze, aby przenieść działania z poszczególnych przejść do osobnego menu działań:

ArticleListItemRow(
    modifier = Modifier
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = "Open article",
                    action = {
                        openArticle()
                        true
                    }
                ),
                CustomAccessibilityAction(
                    label = "Add to bookmarks",
                    action = {
                        addToBookmarks()
                        true
                    }
                ),
            )
        }
) {
    Article(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = openArticle,
    )
    BookmarkButton(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = addToBookmarks,
    )
}

W takich przypadkach musisz ręcznie wyczyścić pierwotną semantykę podrzędnych za pomocą modyfikatora clearAndSetSemantics, ponieważ przenosisz je do działań niestandardowych.

Na przykład w przypadku Switch Access po wybraniu kontenera otwiera się menu z dostępnymi działaniami:

Podświetlenie elementu na liście artykułów w Switch Access
Rysunek 6. Podświetlenie elementu na liście artykułów w usłudze Switch Access.
Wizualizacja menu czynności Switch Access
Rysunek 7. Wizualizacja menu czynności Switch Access

Drzewo semantyczne

Kompozycja opisuje interfejs użytkownika aplikacji i jest generowana przez uruchomione komponenty. Kompozycja to struktura drzewikowa, która składa się z komponentów opisujących Twój interfejs.

Oprócz kompozycji istnieje równoległe drzewo, zwane drzewem semantycznym. To drzewo opisuje interfejs użytkownika w sposób alternatywny, zrozumiały dla usług ułatwień dostępu i ramy testowania. Usługi ułatwień dostępu używają drzewa do opisania aplikacji użytkownikom o konkretnych potrzebach. Platforma testowa korzysta z drzewa do interakcji z aplikacją i formułowania twierdzeń na jej temat. Drzewo semantyczne nie zawiera informacji o tym, jak rysować komponenty, ale zawiera informacje o semantycznym znaczeniu komponentów.

Typowa hierarchia UI i jej drzewo semantyczne
Rysunek 8. Typowa hierarchia UI i jej drzewo semantyczne.

Jeśli Twoja aplikacja składa się z elementów składanych i modyfikatorów z fundamentów Compose oraz biblioteki Material, drzewo semantyczne jest wypełniane i generowane automatycznie. Jednak gdy dodajesz niestandardowe komponenty na niższym poziomie, musisz ręcznie podać ich semantykę. Mogą też wystąpić sytuacje, w których drzewo nie odzwierciedla prawidłowo lub w pełni znaczenia elementów na ekranie. W takim przypadku możesz je dostosować.

Weź pod uwagę na przykład ten niestandardowy komponent kalendarza:

Kalendarz niestandardowy z możliwością wyboru elementów dnia
Rysunek 9. Niestandardowy kalendarz z elementami dnia do wyboru.

W tym przykładzie cały kalendarz jest implementowany jako pojedyncza kompozycja niskiego poziomu, która korzysta z kompozycji Layout i rysuje bezpośrednio do Canvas. Jeśli nie zrobisz nic więcej, usługi ułatwień dostępu nie otrzymają wystarczających informacji o treści składanego i wybranych przez użytkownika elementach w kalendarzu. Jeśli np. użytkownik kliknie dzień zawierający 17, platforma ułatwień dostępu otrzyma tylko informacje o opisie całej opcji kalendarza. W takim przypadku usługa ułatwień dostępu TalkBack ogłosiłaby „Kalendarz” lub, co jest tylko nieznacznie lepsze, „Kalendarz na kwiecień”, a użytkownik musiałby się domyślać, który dzień został wybrany. Aby ułatwić dostęp do tej usługi, musisz ręcznie dodać informacje semantyczne.

Złączone i niezłączone drzewo

Jak już wspomnieliśmy, każdy element w drzewie interfejsu może mieć ustawione właściwości semantyczne lub nie mieć ich wcale. Jeśli kompozyt nie ma ustawionych właściwości semantycznych, nie jest uwzględniany w drzewie semantycznym. Dzięki temu drzewo semantyczne zawiera tylko węzły, które faktycznie mają znaczenie semantyczne. Czasami jednak, aby przekazać prawidłowe znaczenie tego, co widać na ekranie, warto połączyć pewne poddrzewa węzłów i traktować je jako jedno. Dzięki temu możesz rozpatrywać zestaw węzłów jako całość, zamiast zajmować się poszczególnymi węzłami potomnymi. Zasadniczo każdy węzeł w tym drzewie reprezentuje element, na którym można się skupić podczas korzystania z usług ułatwień dostępu.

Przykładem takiego składanego jest Button. Przycisk możesz traktować jako pojedynczy element, nawet jeśli zawiera on wiele węzłów podrzędnych:

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

W drzewie semantycznym właściwości potomków przycisku są scalane, a sam przycisk jest w drzewie przedstawiany jako pojedynczy węzeł liściasty:

Połączona reprezentacja semantyczna pojedynczego liścia
Rysunek 10. Połączona reprezentacja semantyczna pojedynczego wierzchołka.

Elementy składane i modyfikatory mogą wskazywać, że chcą scalić właściwości semantyczne swoich potomków, wywołując funkcję Modifier.semantics (mergeDescendants = true) {}. Ustawienie tej właściwości na true wskazuje, że właściwości semantyczne powinny zostać scalone. W przykładzie Button kompozyt Button używa wewnętrznie modyfikatora clickable, który zawiera modyfikator semantics. Dlatego potomne węzły przycisku są scalane. Aby dowiedzieć się więcej o tym, kiedy należy zmienić zachowanie podczas łączenia w komponowalnym elemencie, przeczytaj dokumentację dotyczącą ułatwień dostępu.

Ta właściwość jest ustawiona w przypadku kilku modyfikatorów i komponentów w bibliotekach Foundation i Material Compose. Na przykład modyfikatory clickable i toggleable automatycznie scalą swoje potomki. Ponadto kompozyt ListItem scali swoje potomki.

Sprawdzanie drzewa

Drzewo semantyczne to w istocie 2 różne drzewa. Istnieje scalony semantyczny drzewo, które scala potomne węzły, gdy parametr mergeDescendants ma wartość true. Dostępne jest też drzewo semantyczne bez scalania, które nie stosuje scalania, ale zachowuje wszystkie węzły. Usługi ułatwień dostępu korzystają z drzewa nieskreślonego i stosują własne algorytmy scalania, biorąc pod uwagę usługę mergeDescendants. Platforma testowa domyślnie używa scalonego drzewa.

Możesz sprawdzić oba drzewa za pomocą metody printToLog(). Domyślnie, podobnie jak w poprzednich przykładach, zgrywane jest złączone drzewo. Aby zamiast tego wydrukować drzewo niescalone, ustaw parametr useUnmergedTree dopasowania onRoot() na true:

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

Kontroler układu umożliwia wyświetlanie złączonego i niezłączonego drzewa semantycznego. Wystarczy, że wybierzesz preferowane drzewo w filtrze widoku:

opcje widoku w inspektorze układu, które umożliwiają wyświetlanie złączonego i niezłączonego drzewa semantycznego;
Rysunek 11. Opcje widoku w inspektorze układu, które umożliwiają wyświetlanie złączonego i niezłączonego drzewa semantycznego.

W przypadku każdego węzła w drzewie w panelu właściwości Inspektor układu wyświetla zarówno złączoną semantykę, jak i semantykę ustawioną dla tego węzła:

Właściwości semantyczne scalone i zdefiniowane
Rysunek 12. Właściwości semantyczne zostały scalone i skonfigurowane.

Domyślnie dopasowywacze w ramach platformy testowej korzystają ze złączonego drzewa semantycznego. Dlatego możesz wchodzić w interakcję z elementem Button, dopasowując tekst wyświetlany wewnątrz:

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

Aby zmienić to zachowanie, ustaw parametr useUnmergedTree w dopasowywaczach na true, tak jak w dopasowywaczu onRoot.

Dostosowywanie drzewa

Jak już wspomnieliśmy, możesz zastąpić lub wyczyścić określone właściwości semantyczne albo zmienić sposób scalania drzewa. Jest to szczególnie przydatne, gdy tworzysz własne komponenty niestandardowe. Jeśli nie skonfigurujesz właściwych właściwości i zachowania podczas scalania, aplikacja może być niedostępna, a testy mogą działać inaczej niż się spodziewasz. Więcej informacji o testowaniu znajdziesz w przewodniku.