Zwiększanie dostępności widoków niestandardowych

Jeśli Twoja aplikacja wymaga komponentu widoku niestandardowego, musisz zwiększyć dostępność tego widoku. Aby ułatwić dostęp do widoku niestandardowego, wykonaj te czynności, jak opisano na tej stronie:

  • Obsługuj kliknięcia kontrolera kierunkowego.
  • Wdróż metody interfejsu Accessibility API.
  • Wyślij obiekty (AccessibilityEvent) specyficzne do widoku niestandardowego.
  • Wypełnij pola AccessibilityEvent i AccessibilityNodeInfo na potrzeby widoku.

Obsługuj kliknięcia kontrolera kierunkowego

Na większości urządzeń kliknięcie widoku za pomocą kontrolera kierunkowego wysyła do aktywnego widoku KeyEvent z wartością KEYCODE_DPAD_CENTER. Wszystkie standardowe widoki Androida prawidłowo obsługują właściwość KEYCODE_DPAD_CENTER. Tworząc własny element sterujący View, upewnij się, że to zdarzenie będzie działać tak samo jak kliknięcie widoku na ekranie dotykowym.

Element sterujący musi traktować zdarzenie KEYCODE_ENTER tak samo jak element KEYCODE_DPAD_CENTER. Ułatwia to użytkownikom obsługę pełnej klawiatury.

Wdrażanie metod interfejsu Accessibility API

Zdarzenia ułatwień dostępu to komunikaty o interakcjach użytkowników z komponentami interfejsu wizualnego aplikacji. Komunikaty te są obsługiwane przez usługi ułatwień dostępu, które wykorzystują informacje zawarte w tych wydarzeniach do generowania dodatkowych opinii i promptów. Metody ułatwień dostępu wchodzą w skład klas View i View.AccessibilityDelegate. Można to zrobić w następujący sposób:

dispatchPopulateAccessibilityEvent()
System wywołuje tę metodę, gdy Twój widok niestandardowy generuje zdarzenie związane z ułatwieniami dostępu. Domyślna implementacja tej metody wywołuje w przypadku tego widoku danych metodę onPopulateAccessibilityEvent(), a następnie metodę dispatchPopulateAccessibilityEvent() w przypadku każdego widoku podrzędnego.
onInitializeAccessibilityEvent()
System wywołuje tę metodę, aby uzyskać dodatkowe informacje o stanie widoku danych wykraczające poza tekst. Jeśli Twój widok niestandardowy zapewnia interaktywną kontrolę wykraczającą poza prosty interfejs TextView lub Button, zastąp tę metodę i ustaw dodatkowe informacje o widoku, takie jak typ pola hasła, typ pola wyboru lub stany, które zapewniają użytkownikowi interakcję lub opinię o zdarzeniu. Jeśli zastąpisz tę metodę, wywołaj jej superimplementację i zmodyfikuj tylko te właściwości, które nie są ustawione przez klasę super.
onInitializeAccessibilityNodeInfo()
Ta metoda udostępnia usługom ułatwień dostępu informacje o stanie widoku. Domyślna implementacja View ma standardowy zestaw właściwości widoku, ale jeśli Twój widok niestandardowy zapewnia interaktywną kontrolę wykraczającą poza prosty TextView lub Button, zastąp tę metodę i umieść dodatkowe informacje o widoku danych w obiekcie AccessibilityNodeInfo obsługiwanym przez tę metodę.
onPopulateAccessibilityEvent()
Ta metoda ustawia prompt odczytywany w widoku AccessibilityEvent. Nazwa jest też wywoływana, jeśli widok jest podrzędny względem widoku, który generuje zdarzenie ułatwień dostępu.
onRequestSendAccessibilityEvent()
System wywołuje tę metodę, gdy element podrzędny Twojego widoku danych wygeneruje AccessibilityEvent. Dzięki temu widokowi nadrzędnemu można uzupełnić zdarzenie ułatwień dostępu o dodatkowe informacje. Tę metodę należy stosować tylko wtedy, gdy widok niestandardowy może zawierać widoki podrzędne, a widok nadrzędny może dostarczać informacje kontekstowe do zdarzenia ułatwień dostępu przydatne dla usług ułatwień dostępu.
sendAccessibilityEvent()
System wywołuje tę metodę, gdy użytkownik wykonuje działanie związane z widokiem danych. Zdarzenie jest klasyfikowane według typu działania użytkownika, np. TYPE_VIEW_CLICKED. Ogólnie rzecz biorąc, musisz wysyłać AccessibilityEvent przy każdej zmianie zawartości widoku niestandardowego.
sendAccessibilityEventUnchecked()
Ta metoda jest używana, gdy kod wywołujący musi bezpośrednio kontrolować sprawdzanie pod kątem włączenia ułatwień dostępu na urządzeniu (AccessibilityManager.isEnabled()). Jeśli wdrożysz tę metodę, wykonaj wywołanie tak, jakby były włączone ułatwienia dostępu, niezależnie od ustawienia systemu. Zwykle nie musisz implementować tej metody w przypadku widoku niestandardowego.

Aby zapewnić obsługę ułatwień dostępu, zastąp i zaimplementuj poprzednie metody ułatwień dostępu bezpośrednio w klasie widoku niestandardowego.

W przypadku klasy widoku niestandardowego wdróż przynajmniej te metody ułatwień dostępu:

  • dispatchPopulateAccessibilityEvent()
  • onInitializeAccessibilityEvent()
  • onInitializeAccessibilityNodeInfo()
  • onPopulateAccessibilityEvent()

Więcej informacji o implementowaniu tych metod znajdziesz w sekcji o wypełnianiu zdarzeń ułatwień dostępu.

Wysyłanie zdarzeń związanych z ułatwieniami dostępu

W zależności od specyfiki widoku niestandardowego może być konieczne wysyłanie obiektów AccessibilityEvent w różnych momentach lub w przypadku zdarzeń, które nie są obsługiwane przez domyślną implementację. Klasa View udostępnia domyślną implementację tych typów zdarzeń:

Ogólnie rzecz biorąc, musisz wysyłać AccessibilityEvent za każdym razem, gdy zmieni się zawartość widoku niestandardowego. Jeśli np. implementujesz niestandardowy pasek z suwakiem, który umożliwia użytkownikowi wybranie wartości liczbowej przez naciśnięcie klawisza strzałki w lewo lub w prawo, zawsze przy każdej zmianie wartości suwaka Twój widok niestandardowy musi wywoływać zdarzenie TYPE_VIEW_TEXT_CHANGED. Ten przykładowy kod pokazuje, jak używać metody sendAccessibilityEvent() do zgłaszania tego zdarzenia.

Kotlin

override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
    return when(keyCode) {
        KeyEvent.KEYCODE_DPAD_LEFT -> {
            currentValue--
            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
            true
        }
        ...
    }
}

Java

@Override
public boolean onKeyUp (int keyCode, KeyEvent event) {
    if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
        currentValue--;
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
        return true;
    }
    ...
}

Wypełnianie zdarzeń ułatwień dostępu

Każdy element AccessibilityEvent ma zestaw wymaganych właściwości opisujących bieżący stan widoku. Obejmują one nazwę klasy widoku, opis treści i stan sprawdzenia. Konkretne właściwości wymagane w przypadku każdego typu zdarzenia są opisane w dokumentacji referencyjnej AccessibilityEvent.

Implementacja View podaje domyślne wartości tych wymaganych właściwości. Wiele z tych wartości, w tym nazwa klasy i sygnatura czasowa zdarzenia, jest ustawianych automatycznie. Jeśli tworzysz niestandardowy komponent widoku, musisz podać informacje o jego zawartości i cechach. Mogą to być proste jak etykieta przycisku i mogą obejmować dodatkowe informacje o stanie, które chcesz dodać do zdarzenia.

Do wypełniania lub modyfikowania informacji w elemencie AccessibilityEvent używaj metod onPopulateAccessibilityEvent() i onInitializeAccessibilityEvent(). Używaj metody onPopulateAccessibilityEvent() do dodawania lub modyfikowania tekstu zdarzenia. Dzięki temu usługi ułatwień dostępu takie jak TalkBack przechodzą na prompty dźwiękowe. Aby podać dodatkowe informacje o zdarzeniu, np. stan wyboru widoku, użyj metody onInitializeAccessibilityEvent().

Dodatkowo zaimplementuj metodę onInitializeAccessibilityNodeInfo(). Usługi ułatwień dostępu korzystają z obiektów AccessibilityNodeInfo wypełnianych przez tę metodę, aby badać hierarchię widoków, która po otrzymaniu zdarzenia ułatwień dostępu generuje zdarzenie związane z ułatwieniami dostępu, i przekazywać użytkownikom odpowiednie informacje zwrotne.

Poniższy przykładowy kod pokazuje, jak zastąpić te trzy metody w widoku:

Kotlin

override fun onPopulateAccessibilityEvent(event: AccessibilityEvent?) {
    super.onPopulateAccessibilityEvent(event)
    // Call the super implementation to populate its text for the
    // event. Then, add text not present in a super class.
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        event?.text?.add(text)
    }
}

override fun onInitializeAccessibilityEvent(event: AccessibilityEvent?) {
    super.onInitializeAccessibilityEvent(event)
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event?.isChecked = isChecked()
}

override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) {
    super.onInitializeAccessibilityNodeInfo(info)
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info?.isCheckable = true
    info?.isChecked = isChecked()
    // You typically only need to add the text for the custom view.
    if (text?.isNotEmpty() == true) {
        info?.text = text
    }
}

Java

@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    super.onPopulateAccessibilityEvent(event);
    // Call the super implementation to populate its text for the
    // event. Then, add the text not present in a super class.
    // You typically only need to add the text for the custom view.
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        event.getText().add(text);
    }
}

@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    super.onInitializeAccessibilityEvent(event);
    // Call the super implementation to let super classes
    // set appropriate event properties. Then, add the new checked
    // property that is not supported by a super class.
    event.setChecked(isChecked());
}

@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    super.onInitializeAccessibilityNodeInfo(info);
    // Call the super implementation to let super classes set
    // appropriate info properties. Then, add the checkable and checked
    // properties that are not supported by a super class.
    info.setCheckable(true);
    info.setChecked(isChecked());
    // You typically only need to add the text for the custom view.
    CharSequence text = getText();
    if (!TextUtils.isEmpty(text)) {
        info.setText(text);
    }
}

Te metody możesz zaimplementować bezpośrednio w klasie widoku niestandardowego.

Dostosowywanie kontekstu ułatwień dostępu

Usługi ułatwień dostępu mogą sprawdzać hierarchię widoków elementu interfejsu, który generuje zdarzenie ułatwień dostępu. Dzięki temu usługi ułatwień dostępu mogą dostarczać użytkownikom bardziej szczegółowe informacje kontekstowe.

Czasami usługi ułatwień dostępu nie mogą uzyskać odpowiednich informacji z hierarchii widoków. Przykładem jest niestandardowy element sterujący interfejsu, który zawiera co najmniej 2 osobne klikalne obszary, takie jak element sterujący kalendarza. W takim przypadku usługi nie mogą uzyskać odpowiednich informacji, ponieważ klikalne podsekcje nie są częścią hierarchii widoków.

Rysunek 1. Niestandardowy widok kalendarza z elementami dnia do wyboru.

W przykładzie na Rysunku 1 cały kalendarz jest zaimplementowany jako pojedynczy widok, więc usługi ułatwień dostępu nie otrzymują wystarczającej ilości informacji o zawartości widoku i wyborze użytkownika w tym widoku, chyba że programista dostarczy dodatkowe informacje. Jeśli na przykład użytkownik kliknie dzień oznaczony etykietą 17, platforma ułatwień dostępu otrzyma tylko opis dla całego elementu sterującego kalendarza. W takim przypadku usługa ułatwień dostępu TalkBack odczyta komunikat „Kalendarz” lub „Kalendarz kwietniowy”, a użytkownik nie wie, który dzień został wybrany.

Aby zapewnić odpowiedni kontekst dla usług ułatwień dostępu w takich sytuacjach, platforma umożliwia określenie hierarchii widoków wirtualnych. Hierarchia widoków wirtualnych to sposób, w jaki deweloperzy aplikacji mogą zapewnić uzupełniającą hierarchię widoków danych w ramach usług ułatwień dostępu, które są bardziej zbliżone do informacji na ekranie. Dzięki temu usługi ułatwień dostępu mogą dostarczać użytkownikom bardziej przydatne informacje kontekstowe.

Inną sytuacją, w której hierarchia widoku wirtualnego może być potrzebna, jest interfejs zawierający zestaw elementów sterujących View, które mają ściśle powiązane funkcje. W takiej sytuacji działanie jednego lub więcej elementów wpływa na zawartość co najmniej jednego elementu, np. selektora liczb z oddzielnymi przyciskami w górę i w dół. W takim przypadku usługi ułatwień dostępu nie mogą uzyskać wystarczających informacji, ponieważ działanie jednego z elementów sterujących powoduje zmianę treści w innej, a relacja tych ustawień z usługą może być niewidoczna.

Aby rozwiązać ten problem, zgrupuj powiązane elementy sterujące w widok zawierający i podaj hierarchię widoku wirtualnego z tego kontenera, aby jasno przedstawiać informacje i zachowanie zapewniane przez te opcje.

Aby udostępnić wirtualny widok hierarchii widoków, zastąp metodę getAccessibilityNodeProvider() w widoku niestandardowym lub grupie widoków i zwróć implementację AccessibilityNodeProvider. Hierarchię widoków wirtualnych możesz wdrożyć, korzystając z biblioteki pomocy, używając metody ViewCompat.getAccessibilityNodeProvider() i udostępniając implementację za pomocą AccessibilityNodeProviderCompat.

Aby uprościć przekazywanie informacji do usług ułatwień dostępu i zarządzanie nimi, możesz zamiast tego wdrożyć ExploreByTouchHelper. Zawiera element AccessibilityNodeProviderCompat, który można dołączyć jako element AccessibilityDelegateCompat widoku, wywołując stronę setAccessibilityDelegate. Przykład: ExploreByTouchHelperActivity. Z kolei ExploreByTouchHelper korzystają też widżety platformy, takie jak CalendarView, w widoku podrzędnym SimpleMonthView.

Obsługa niestandardowych zdarzeń dotknięcia

Ustawienia widoku niestandardowego mogą wymagać niestandardowego działania zdarzeń dotknięcia, jak pokazano w tych przykładach.

Zdefiniuj działania oparte na kliknięciach

Jeśli widżet korzysta z interfejsu OnClickListener lub OnLongClickListener, system automatycznie wykonuje działania ACTION_CLICK i ACTION_LONG_CLICK. Jeśli Twoja aplikacja używa bardziej niestandardowego widżetu, który opiera się na interfejsie OnTouchListener, zdefiniuj niestandardowe moduły obsługi działań ułatwień dostępu opartych na kliknięciu. Aby to zrobić, dla każdego działania wywołaj metodę replaceAccessibilityAction(), tak jak w tym fragmencie kodu:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Assumes that the widget is designed to select text when tapped, and selects
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
        binding.textSelectWidget,
        ACTION_CLICK,
        getString(R.string.select)
    ) { view, commandArguments ->
        selectText()
    }

    ViewCompat.replaceAccessibilityAction(
        binding.textSelectWidget,
        ACTION_LONG_CLICK,
        getString(R.string.select_all)
    ) { view, commandArguments ->
        selectAllText()
    }
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    // Assumes that the widget is designed to select text when tapped, and select
    // all text when tapped and held. In its strings.xml file, this app sets
    // "select" to "Select" and "select_all" to "Select all".
    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_CLICK,
            getString(R.string.select),
            (view, commandArguments) -> selectText());

    ViewCompat.replaceAccessibilityAction(
            binding.textSelectWidget,
            ACTION_LONG_CLICK,
            getString(R.string.select_all),
            (view, commandArguments) -> selectAllText());
}

Tworzenie niestandardowych zdarzeń kliknięcia

Element sterujący może korzystać z metody odbiornika onTouchEvent(MotionEvent), aby wykrywać zdarzenia ACTION_DOWN i ACTION_UP oraz wywoływać specjalne zdarzenie kliknięcia. Aby zachować zgodność z usługami ułatwień dostępu, kod obsługujący to niestandardowe zdarzenie kliknięcia musi spełniać te warunki:

  1. Wygeneruj odpowiedni AccessibilityEvent dla zinterpretowanego działania kliknięcia.
  2. Włącz usługi ułatwień dostępu, aby umożliwić wykonywanie niestandardowych kliknięć użytkownikom, którzy nie mogą korzystać z ekranu dotykowego.

Aby sprawnie sprostać tym wymaganiom, Twój kod musi zastąpić metodę performClick(), która musi wywołać superimplementację tej metody, a następnie wykonać wszelkie działania wymagane przez zdarzenie kliknięcia. Po wykryciu niestandardowego działania kliknięcia kod ten musi wywołać Twoją metodę performClick(). Poniższy przykładowy kod ilustruje ten wzorzec.

Kotlin

class CustomTouchView(context: Context) : View(context) {

    var downTouch = false

    override fun onTouchEvent(event: MotionEvent): Boolean {
        super.onTouchEvent(event)

        // Listening for the down and up touch events.
        return when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                downTouch = true
                true
            }

            MotionEvent.ACTION_UP -> if (downTouch) {
                downTouch = false
                performClick() // Call this method to handle the response and
                // enable accessibility services to
                // perform this action for a user who can't
                // tap the touchscreen.
                true
            } else {
                false
            }

            else -> false  // Return false for other touch events.
        }
    }

    override fun performClick(): Boolean {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any.
        super.performClick()

        // Handle the action for the custom click here.

        return true
    }
}

Java

class CustomTouchView extends View {

    public CustomTouchView(Context context) {
        super(context);
    }

    boolean downTouch = false;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        // Listening for the down and up touch events
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downTouch = true;
                return true;

            case MotionEvent.ACTION_UP:
                if (downTouch) {
                    downTouch = false;
                    performClick(); // Call this method to handle the response and
                                    // enable accessibility services to
                                    // perform this action for a user who can't
                                    // tap the touchscreen.
                    return true;
                }
        }
        return false; // Return false for other touch events.
    }

    @Override
    public boolean performClick() {
        // Calls the super implementation, which generates an AccessibilityEvent
        // and calls the onClick() listener on the view, if any.
        super.performClick();

        // Handle the action for the custom click here.

        return true;
    }
}

Poprzedni wzorzec pomaga zapewnić zgodność niestandardowego zdarzenia kliknięcia z usługami ułatwień dostępu dzięki użyciu metody performClick() do generowania zdarzenia ułatwień dostępu i zapewnienia punktu wejścia usług ułatwień dostępu, które działają w imieniu użytkownika wykonującego zdarzenie kliknięcia niestandardowego.