Zasady poprawy dostępności aplikacji (widoki)

Pojęcia i implementacja w Jetpack Compose

Aby ułatwić korzystanie z urządzeń użytkownikom z niepełnosprawnościami, platforma Android umożliwia tworzenie usług ułatwień dostępu, które mogą prezentować treści z aplikacji i obsługiwać je w imieniu użytkowników.

Android udostępnia kilka systemowych usług ułatwień dostępu, w tym:

  • TalkBack: pomaga osobom niedowidzącym i niewidomym. Odczytuje treści za pomocą syntezatora mowy i wykonuje działania w aplikacji w odpowiedzi na gesty użytkownika.
  • Switch Access: pomaga osobom z niepełnosprawnością ruchową. Podświetla elementy interaktywne i wykonuje działania w odpowiedzi na naciśnięcie przycisku przez użytkownika. Umożliwia sterowanie urządzeniem za pomocą tylko 1 lub 2 przycisków.

Aby osoby z niepełnosprawnościami mogły bez problemu korzystać z Twojej aplikacji, musi ona być zgodna ze sprawdzonymi metodami opisanymi na tej stronie, które bazują na wytycznych opisanych w artykule Ułatwianie dostępu do aplikacji.

Elementy etykiety

Ważne jest, aby udostępniać użytkownikom przydatne i opisowe etykiety każdego interaktywnego elementu interfejsu w aplikacji. Każda etykieta musi wyjaśniać znaczenie i przeznaczenie danego elementu. Czytniki ekranu, takie jak TalkBack, mogą odczytywać te etykiety użytkownikom.

W większości przypadków opis elementu interfejsu podajesz w pliku zasobu układu, który zawiera ten element. Zwykle etykiety dodaje się za pomocą atrybutu contentDescription, jak wyjaśniono w przewodniku zwiększającym dostępność aplikacji. W kolejnych sekcjach opisujemy kilka innych technik etykietowania.

Elementy, które można edytować

Podczas oznaczania elementów możliwych do edytowania, takich jak obiekty EditText, warto wyświetlać tekst, który zawiera przykład prawidłowych danych wejściowych w samym elemencie, a także udostępniać ten tekst czytnikom ekranu. W takich sytuacjach możesz użyć atrybutu android:hint, jak pokazano w tym fragmencie kodu:

<!-- The hint text for en-US locale would be
     "Apartment, suite, or building". -->
<EditText
   android:id="@+id/addressLine2"
   android:hint="@string/aptSuiteBuilding" ... />

W takiej sytuacji obiekt View musi mieć atrybut android:labelFor ustawiony na identyfikator elementu EditText. Więcej informacji znajdziesz w sekcji poniżej.

Pary elementów, w których jeden opisuje drugi

Często element EditText ma odpowiadający mu obiekt View, który opisuje, co użytkownicy muszą wpisać w elemencie EditText. Możesz wskazać tę relację, ustawiając atrybut android:labelFor obiektu View.

Przykład etykietowania takich par elementów znajdziesz w tym fragmencie kodu:

<!-- Label text for en-US locale would be "Username:" -->
<TextView
   android:id="@+id/usernameLabel" ...
   android:text="@string/username"
   android:labelFor="@+id/usernameEntry" />

<EditText
   android:id="@+id/usernameEntry" ... />

<!-- Label text for en-US locale would be "Password:" -->
<TextView
   android:id="@+id/passwordLabel" ...
   android:text="@string/password
   android:labelFor="@+id/passwordEntry" />

<EditText
   android:id="@+id/passwordEntry"
   android:inputType="textPassword" ... />

Elementy w kolekcji

Podczas dodawania etykiet do elementów kolekcji każda etykieta musi być niepowtarzalna. Dzięki temu usługi ułatwień dostępu w systemie mogą odwoływać się do dokładnie jednego elementu na ekranie podczas ogłaszania etykiety. Dzięki temu użytkownicy wiedzą, kiedy przechodzą między elementami interfejsu lub kiedy przenoszą fokus na element, który już odkryli.

W szczególności dodaj dodatkowy tekst lub informacje kontekstowe do elementów w ponownie używanych układach, takich jak obiekty RecyclerView, aby każdy element podrzędny był jednoznacznie identyfikowany.

Aby to zrobić, ustaw opis treści w ramach implementacji adaptera, jak pokazano w tym fragmencie kodu:

Kotlin

data class MovieRating(val title: String, val starRating: Integer)

class MyMovieRatingsAdapter(private val myData: Array<MovieRating>):
        RecyclerView.Adapter<MyMovieRatingsAdapter.MyRatingViewHolder>() {

    class MyRatingViewHolder(val ratingView: ImageView) :
            RecyclerView.ViewHolder(ratingView)

    override fun onBindViewHolder(holder: MyRatingViewHolder, position: Int) {
        val ratingData = myData[position]
        holder.ratingView.contentDescription = "Movie ${position}: " +
                "${ratingData.title}, ${ratingData.starRating} stars"
    }
}

Java

public class MovieRating {
    private String title;
    private int starRating;
    // ...
    public String getTitle() { return title; }
    public int getStarRating() { return starRating; }
}

public class MyMovieRatingsAdapter
        extends RecyclerView.Adapter<MyAdapter.MyRatingViewHolder> {
    private MovieRating[] myData;


    public static class MyRatingViewHolder extends RecyclerView.ViewHolder {
        public ImageView ratingView;
        public MyRatingViewHolder(ImageView iv) {
            super(iv);
            ratingView = iv;
        }
    }

    @Override
    public void onBindViewHolder(MyRatingViewHolder holder, int position) {
        MovieRating ratingData = myData[position];
        holder.ratingView.setContentDescription("Movie " + position + ": " +
                ratingData.getTitle() + ", " + ratingData.getStarRating() +
                " stars")
    }
}

Grupy powiązanych treści

Jeśli aplikacja wyświetla kilka elementów interfejsu, które tworzą naturalną grupę, np. szczegóły utworu lub atrybuty wiadomości, umieść te elementy w kontenerze, który zwykle jest podklasą ViewGroup. Ustaw atrybut android:screenReaderFocusable obiektu kontenera na true, a atrybut android:focusable każdego obiektu wewnętrznego na false. Dzięki temu usługi ułatwień dostępu mogą prezentować opisy treści elementów wewnętrznych jeden po drugim w jednym komunikacie. Konsolidacja powiązanych elementów pomaga użytkownikom technologii wspomagającej osoby z niepełnosprawnością łatwiej znajdować informacje na ekranie.

Poniższy fragment kodu zawiera części treści, które są ze sobą powiązane, więc element kontenera, czyli instancja ConstraintLayout, ma atrybut android:screenReaderFocusable ustawiony na true, a elementy wewnętrzne TextView mają atrybut android:focusable ustawiony na false:

<!-- In response to a single user interaction, accessibility services announce
     both the title and the artist of the song. -->
<ConstraintLayout
    android:id="@+id/song_data_container" ...
    android:screenReaderFocusable="true">

    <TextView
        android:id="@+id/song_title" ...
        android:focusable="false"
        android:text="@string/my_song_title" />
    <TextView
        android:id="@+id/song_artist"
        android:focusable="false"
        android:text="@string/my_songwriter" />
</ConstraintLayout>

Usługi ułatwień dostępu odczytują opisy elementów wewnętrznych w jednym komunikacie, dlatego ważne jest, aby każdy opis był jak najkrótszy, ale jednocześnie przekazywał znaczenie elementu.

Uwaga: na ogół nie należy tworzyć opisu treści dla grupy przez agregowanie tekstu jej elementów podrzędnych. W takim przypadku opis grupy staje się podatny na błędy, a gdy tekst elementu podrzędnego ulegnie zmianie, opis grupy może już nie pasować do widocznego tekstu.

W kontekście listy lub siatki czytnik ekranu może łączyć tekst węzłów tekstowych podrzędnych elementu listy lub siatki. Najlepiej nie modyfikować tego ogłoszenia.

Zagnieżdżone grupy

Jeśli interfejs aplikacji zawiera informacje wielowymiarowe, np. listę wydarzeń festiwalowych z podziałem na dni, użyj atrybutu android:screenReaderFocusable w wewnętrznych kontenerach grup. Ten schemat etykietowania zapewnia dobrą równowagę między liczbą komunikatów potrzebnych do odkrycia zawartości ekranu a długością każdego komunikatu.

Poniższy fragment kodu pokazuje jedną z metod oznaczania grup w większych grupach:

<!-- In response to a single user interaction, accessibility services
     announce the events for a single stage only. -->
<ConstraintLayout
    android:id="@+id/festival_event_table" ... >
    <ConstraintLayout
        android:id="@+id/stage_a_event_column"
        android:screenReaderFocusable="true">

        <!-- UI elements that describe the events on Stage A. -->

    </ConstraintLayout>
    <ConstraintLayout
        android:id="@+id/stage_b_event_column"
        android:screenReaderFocusable="true">

        <!-- UI elements that describe the events on Stage B. -->

    </ConstraintLayout>
</ConstraintLayout>

Nagłówki w tekście

Niektóre aplikacje używają nagłówków do podsumowywania grup tekstu wyświetlanych na ekranie. Jeśli konkretny element View reprezentuje nagłówek, możesz wskazać jego przeznaczenie dla usług ułatwień dostępu, ustawiając atrybut android:accessibilityHeading elementu na true.

Użytkownicy usług ułatwień dostępu mogą przechodzić między nagłówkami zamiast między akapitami lub słowami. Ta elastyczność poprawia nawigację tekstową.

Tytuły paneli ułatwień dostępu

W Androidzie 9 (API na poziomie 28) i nowszych wersjach możesz podać tytuły paneli ekranu, które są przyjazne dla osób z niepełnosprawnością. Ze względu na ułatwienia dostępu panel to wizualnie odrębna część okna, np. zawartość fragmentu. Aby usługi ułatwień dostępu mogły zrozumieć zachowanie panelu podobne do okna, nadaj panelom aplikacji opisowe tytuły. Usługi ułatwień dostępu mogą wtedy przekazywać użytkownikom bardziej szczegółowe informacje, gdy zmieni się wygląd lub zawartość panelu.

Aby określić tytuł panelu, użyj atrybutu android:accessibilityPaneTitle, jak pokazano w tym fragmencie kodu:

<!-- Accessibility services receive announcements about content changes
     that are scoped to either the "shopping cart view" section (top) or
     "browse items" section (bottom) -->
<MyShoppingCartView
     android:id="@+id/shoppingCartContainer"
     android:accessibilityPaneTitle="@string/shoppingCart" ... />

<MyShoppingBrowseView
     android:id="@+id/browseItemsContainer"
     android:accessibilityPaneTitle="@string/browseProducts" ... />

Elementy dekoracyjne

Jeśli element interfejsu istnieje tylko w celu zapewnienia odstępu lub wyglądu, ustaw jego atrybut android:importantForAccessibility na "no".

Dodawanie działań związanych z ułatwieniami dostępu

Ważne jest, aby użytkownicy usług ułatwień dostępu mogli łatwo wykonywać wszystkie ścieżki użytkownika w aplikacji. Jeśli na przykład użytkownik może przesunąć element na liście, to działanie może być też udostępnione usługom ułatwień dostępu, aby użytkownicy mieli alternatywny sposób na wykonanie tej samej ścieżki.

Udostępnianie wszystkich działań

Użytkownik TalkBack, Voice Access lub Switch Access może potrzebować alternatywnych sposobów na wykonanie określonych ścieżek użytkownika w aplikacji. W przypadku działań związanych z gestami, takimi jak przeciąganie i upuszczanie lub przesuwanie, aplikacja może udostępniać działania w sposób dostępny dla użytkowników usług ułatwień dostępu.

Za pomocą działań związanych z ułatwieniami dostępu aplikacja może udostępniać użytkownikom alternatywne sposoby wykonania działania.

Jeśli na przykład aplikacja umożliwia użytkownikom przesuwanie elementów, możesz też udostępnić tę funkcję za pomocą niestandardowego działania ułatwień dostępu, np. w ten sposób:

Kotlin

ViewCompat.addAccessibilityAction(
    // View to add accessibility action
    itemView,
    // Label surfaced to user by an accessibility service
    getText(R.id.archive)
) { _, _ ->
    // Same method executed when swiping on itemView
    archiveItem()
    true
}

Java

ViewCompat.addAccessibilityAction(
    // View to add accessibility action
    itemView,
    // Label surfaced to user by an accessibility service
    getText(R.id.archive),
    (view, arguments) -> {
        // Same method executed when swiping on itemView
        archiveItem();
        return true;
    }
);

With the custom accessibility action implemented, users can access the action through the actions menu.

Make available actions understandable

When a view supports actions such as touch & hold, an accessibility service such as TalkBack announces it as "Double tap and hold to long press."

This generic announcement doesn't give the user any context about what a touch & hold action does.

To make this announcement more descriptive, you can replace the accessibility actions announcement like so:

Kotlin

ViewCompat.replaceAccessibilityAction(
    // View that contains touch & hold action
    itemView,
    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
    // Announcement read by TalkBack to surface this action
    getText(R.string.favorite),
    null
)

Java

ViewCompat.replaceAccessibilityAction(
    // View that contains touch & hold action
    itemView,
    AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
    // Announcement read by TalkBack to surface this action
    getText(R.string.favorite),
    null
);

This results in TalkBack announcing "Double tap and hold to favorite," helping users understand the purpose of the action.

Extend system widgets

Note: When you design your app's UI, use or extend system-provided widgets that are as far down Android's class hierarchy as possible. System-provided widgets that are far down the hierarchy already have most of the accessibility capabilities your app needs. It's easier to extend these system-provided widgets than to create your own from the more generic View, ViewCompat, Canvas, and CanvasCompat classes.

If you must extend View or Canvas directly, which might be necessary for a highly customized experience or a game level, see Make custom views more accessible.

This section uses the example of implementing a special type of Switch called TriSwitch while following best practices around extending system widgets. A TriSwitch object works similarly to a Switch object, except that each instance of TriSwitch allows the user to toggle among three possible states.

Extend from far down the class hierarchy

The Switch object inherits from several framework UI classes in its hierarchy:

View
 TextView
   Button
     CompoundButton
       Switch

Najlepiej, aby nowa klasa TriSwitch rozszerzała bezpośrednio klasę Switch. Dzięki temu platforma ułatwień dostępu na Androidzie zapewnia większość funkcji ułatwień dostępu, których potrzebuje klasa TriSwitch:

  • Działania związane z ułatwieniami dostępu: informacje dla systemu o tym, jak usługi ułatwień dostępu mogą emulować każde możliwe dane wejściowe użytkownika wykonane na obiekcie TriSwitch. (Odziedziczone z: View).
  • Wydarzenia związane z ułatwieniami dostępu: informacje dla usług ułatwień dostępu o każdym możliwym sposobie zmiany wyglądu obiektu TriSwitch podczas odświeżania lub aktualizowania ekranu. (Odziedziczone z: View).
  • Charakterystyka: szczegóły dotyczące każdego TriSwitch obiektu, np. zawartość wyświetlanego tekstu. (Odziedziczone z: TextView).
  • Informacje o stanie: opis bieżącego stanu obiektu TriSwitch, np. „zaznaczony” lub „niezaznaczony”. (Odziedziczone z: CompoundButton).
  • Opis tekstowy stanu: tekstowe wyjaśnienie, co oznacza każdy stan. (Odziedziczone z: Switch).

To zachowanie klasy Switch i jej nadklas jest niemal identyczne z zachowaniem obiektów TriSwitch. Dlatego możesz skupić się na zwiększeniu liczby możliwych stanów z dwóch do trzech.

Definiowanie zdarzeń niestandardowych

Gdy rozszerzasz widżet systemowy, prawdopodobnie zmieniasz sposób, w jaki użytkownicy wchodzą z nim w interakcję. Ważne jest, aby zdefiniować te zmiany interakcji, aby usługi ułatwień dostępu mogły aktualizować widżet aplikacji tak, jakby użytkownik wchodził z nim w interakcję bezpośrednio.

Ogólna zasada jest taka, że w przypadku każdego wywołania zwrotnego opartego na widoku, które zastępujesz, musisz też ponownie zdefiniować odpowiednie działanie związane z ułatwieniami dostępu, zastępując metodę ViewCompat.replaceAccessibilityAction(). W testach aplikacji możesz sprawdzić działanie tych przedefiniowanych działań, wywołując funkcję ViewCompat.performAccessibilityAction().

Jak ta zasada może działać w przypadku obiektów TriSwitch

W przeciwieństwie do zwykłego obiektu Switch kliknięcie obiektu TriSwitch powoduje przełączanie między 3 możliwymi stanami. Dlatego należy zaktualizować odpowiednie działanie ACTION_CLICK ułatwień dostępu:

Kotlin

class TriSwitch(context: Context) : Switch(context) {
    // 0, 1, or 2
    var currentState: Int = 0
        private set

    init {
        updateAccessibilityActions()
    }

    private fun updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label) {
            view, args -> moveToNextState()
        })
    }

    private fun moveToNextState() {
        currentState = (currentState + 1) % 3
    }
}

Java

public class TriSwitch extends Switch {
    // 0, 1, or 2
    private int currentState;

    public int getCurrentState() {
        return currentState;
    }

    public TriSwitch() {
        updateAccessibilityActions();
    }

    private void updateAccessibilityActions() {
        ViewCompat.replaceAccessibilityAction(this, ACTION_CLICK,
            action-label, (view, args) -> moveToNextState());
    }

    private void moveToNextState() {
        currentState = (currentState + 1) % 3;
    }
}

Dodatkowe materiały

Więcej informacji o zwiększaniu dostępności aplikacji znajdziesz w tych materiałach:

Codelabs

Posty na blogu