Ułatwianie dostępu do widoków niestandardowych (Widoki)

Pojęcia i implementacja w Jetpack Compose

Jeśli Twoja aplikacja wymaga niestandardowego komponentu widoku, musisz zwiększyć dostępność widoku. Wykonaj te czynności, aby zwiększyć dostępność widoku niestandardowego, zgodnie z opisem na tej stronie:

  • Obsługa kliknięć kontrolera kierunkowego.
  • Zaimplementuj metody interfejsu API ułatwień dostępu.
  • wysyłać AccessibilityEvent obiekty specyficzne dla widoku niestandardowego.
  • Wypełnij pola AccessibilityEventAccessibilityNodeInfo w widoku danych.

Obsługa kliknięć kontrolera kierunkowego

Na większości urządzeń kliknięcie widoku za pomocą kontrolera kierunkowego wysyła KeyEventKEYCODE_DPAD_CENTER do widoku, który jest obecnie aktywny. Wszystkie standardowe widoki Androida obsługują KEYCODE_DPAD_CENTERprawidłowo. Podczas tworzenia niestandardowego elementu sterującegoViewupewnij się, że to zdarzenie ma taki sam efekt jak dotknięcie widoku na ekranie dotykowym.

Element sterujący niestandardowy musi traktować zdarzenie KEYCODE_ENTER KEYCODE_DPAD_CENTERtak samo jak KEYCODE_DPAD_CENTER. Ułatwia to użytkownikom interakcje z pełną klawiaturą.

Implementowanie metod interfejsu API ułatwień dostępu

Zdarzenia związane z ułatwieniami dostępu to wiadomości o interakcjach użytkowników z elementami wizualnego interfejsu aplikacji. Te wiadomości są obsługiwane przez usługi ułatwień dostępu, które wykorzystują informacje z tych zdarzeń do generowania dodatkowych opinii i promptów. Metody ułatwień dostępu są częścią klas ViewView.AccessibilityDelegate. Są to następujące metody:

dispatchPopulateAccessibilityEvent()
System wywołuje tę metodę, gdy widok niestandardowy generuje zdarzenie ułatwień dostępu. Domyślna implementacja tej metody wywołuje funkcję onPopulateAccessibilityEvent() dla tego widoku, a następnie metodę dispatchPopulateAccessibilityEvent() dla każdego elementu podrzędnego tego widoku.
onInitializeAccessibilityEvent()
System wywołuje tę metodę, aby uzyskać dodatkowe informacje o stanie widoku poza treścią tekstową. Jeśli widok niestandardowy zapewnia interaktywną kontrolę wykraczającą poza prosty element 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ą interakcję użytkownika lub informacje zwrotne w zdarzeniu, za pomocą tej metody. Jeśli zastąpisz tę metodę, wywołaj jej implementację superklasy i modyfikuj tylko właściwości, które nie są ustawione przez superklasę.
onInitializeAccessibilityNodeInfo()
Ta metoda przekazuje 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 proste TextView lub Button, zastąp tę metodę i ustaw dodatkowe informacje o widoku w obiekcie AccessibilityNodeInfo obsługiwanym przez tę metodę.
onPopulateAccessibilityEvent()
Ta metoda ustawia tekstowy prompt głosowy AccessibilityEvent dla Twojego widoku. Jest ona też wywoływana, jeśli widok jest elementem podrzędnym widoku, który generuje zdarzenie związane z ułatwieniami dostępu.
onRequestSendAccessibilityEvent()
System wywołuje tę metodę, gdy element podrzędny widoku generuje obiekt AccessibilityEvent. Na tym etapie rodzic może dodać do zdarzenia związanego z ułatwieniami dostępu dodatkowe informacje. Zaimplementuj tę metodę tylko wtedy, gdy widok niestandardowy może zawierać widoki podrzędne i gdy widok nadrzędny może przekazywać informacje kontekstowe do zdarzenia ułatwień dostępu, które są przydatne dla usług ułatwień dostępu.
sendAccessibilityEvent()
System wywołuje tę metodę, gdy użytkownik wykona działanie w widoku. Zdarzenie jest klasyfikowane według typu działania użytkownika, np. TYPE_VIEW_CLICKED. Ogólnie rzecz biorąc, musisz wysłać AccessibilityEvent za każdym razem, gdy zmieni się zawartość widoku niestandardowego.
sendAccessibilityEventUnchecked()
Ta metoda jest używana, gdy kod wywołujący musi bezpośrednio kontrolować sprawdzanie, czy ułatwienia dostępu są włączone na urządzeniu (AccessibilityManager.isEnabled()). Jeśli zaimplementujesz tę metodę, wykonaj wywołanie tak, jakby ułatwienia dostępu były włączone, niezależnie od ustawienia systemowego. Zwykle nie musisz implementować tej metody w przypadku widoku niestandardowego.

Aby zapewnić ułatwienia dostępu, zastąp i wdroż podane wyżej metody ułatwień dostępu bezpośrednio w klasie widoku niestandardowego.

W przypadku niestandardowej klasy widoku zaimplementuj co najmniej te metody ułatwień dostępu:

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

Więcej informacji o implementowaniu tych metod znajdziesz w sekcji wypełniania zdarzeń związanych z dostępnością.

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

W zależności od szczegółów niestandardowego widoku może być konieczne wysyłanie obiektówAccessibilityEvent w różnych momentach lub w przypadku zdarzeń nieobsługiwanych przez domyślną implementację. Klasa View udostępnia domyślną implementację tych typów zdarzeń:

Zasadniczo musisz wysyłać AccessibilityEvent za każdym razem, gdy zmienia się zawartość niestandardowego widoku. Jeśli na przykład wdrażasz niestandardowy suwak, który umożliwia użytkownikowi wybór wartości liczbowej przez naciśnięcie klawisza strzałki w lewo lub w prawo, widok niestandardowy musi emitować zdarzenie TYPE_VIEW_TEXT_CHANGED za każdym razem, gdy zmieni się wartość suwaka. Poniższy przykładowy kod pokazuje, jak użyć metody sendAccessibilityEvent() do zgłoszenia 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ń związanych z ułatwieniami dostępu

Każdy AccessibilityEvent ma zestaw wymaganych właściwości, które opisują bieżący stan widoku. Obejmują one takie elementy jak nazwa klasy widoku, opis treści i stan zaznaczenia. Wymagane właściwości poszczególnych typów zdarzeń zostały opisane w AccessibilityEventdokumentacji referencyjnej.

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

Użyj metod onPopulateAccessibilityEvent() i onInitializeAccessibilityEvent() do wypełniania lub modyfikowania informacji w AccessibilityEvent. Używaj metody onPopulateAccessibilityEvent() do dodawania lub modyfikowania tekstu wydarzenia, który jest przekształcany w komunikaty głosowe przez usługi ułatwień dostępu, takie jak TalkBack. Metoda onInitializeAccessibilityEvent() służy do wypełniania dodatkowych informacji o zdarzeniu, np. stanu wyboru widoku.

Dodatkowo zaimplementuj metodęonInitializeAccessibilityNodeInfo(). Usługi ułatwień dostępu używają obiektów AccessibilityNodeInfo wypełnianych przez tę metodę do badania hierarchii widoków, która generuje zdarzenie ułatwień dostępu po jego otrzymaniu, i przekazywania użytkownikom odpowiednich informacji zwrotnych.

Poniższy przykład kodu pokazuje, jak zastąpić te 3 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);
    }
}

Możesz zaimplementować te metody bezpośrednio w klasie widoku niestandardowego.

Podawanie dostosowanego 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 bogatsze informacje kontekstowe.

Zdarzają się sytuacje, w których usługi ułatwień dostępu nie są w stanie uzyskać wystarczających informacji z hierarchii widoków. Przykładem może być niestandardowy element sterujący interfejsu, który ma co najmniej 2 obszary, w które można kliknąć osobno, np. element sterujący kalendarzem. W takim przypadku usługi nie mogą uzyskać odpowiednich informacji, ponieważ klikalne podsekcje nie są częścią hierarchii widoków.

Rysunek 1. Widok kalendarza niestandardowego z elementami dnia do wyboru.

W przykładzie na ilustracji 1 cały kalendarz jest zaimplementowany jako jeden widok, więc usługi ułatwień dostępu nie otrzymują wystarczających informacji o zawartości widoku i wyborze użytkownika w tym widoku, chyba że deweloper poda dodatkowe informacje. Jeśli na przykład użytkownik kliknie dzień oznaczony numerem 17, platforma ułatwień dostępu otrzyma tylko informacje o całym elemencie sterującym kalendarza. W takim przypadku usługa ułatwień dostępu TalkBack odczytuje „Kalendarz” lub „Kalendarz kwietnia”, a użytkownik nie wie, który dzień jest zaznaczony.

Aby w takich sytuacjach zapewnić usługom ułatwień dostępu odpowiednie informacje kontekstowe, platforma udostępnia sposób na określenie wirtualnej hierarchii widoków. Wirtualna hierarchia widoków to sposób, w jaki deweloperzy aplikacji mogą udostępniać usługom ułatwień dostępu uzupełniającą hierarchię widoków, która dokładniej odzwierciedla informacje wyświetlane na ekranie. Dzięki temu usługi ułatwień dostępu mogą przekazywać użytkownikom bardziej przydatne informacje kontekstowe.

Inną sytuacją, w której może być potrzebna wirtualna hierarchia widoków, jest interfejs użytkownika zawierający zestaw View elementów sterujących o ściśle powiązanych funkcjach, w którym działanie jednego elementu sterującego wpływa na zawartość co najmniej jednego elementu – np. selektor liczb z osobnymi przyciskami w górę i w dół. W takim przypadku usługi ułatwień dostępu nie mogą uzyskać odpowiednich informacji, ponieważ działanie jednego elementu sterującego zmienia zawartość innego, a związek między tymi elementami może nie być dla usługi oczywisty.

Aby sobie z tym poradzić, pogrupuj powiązane elementy sterujące w widoku kontenera i utwórz wirtualną hierarchię widoków z tego kontenera, aby wyraźnie przedstawić informacje i zachowanie zapewniane przez elementy sterujące.

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

Aby uprościć zadanie dostarczania informacji do usług ułatwień dostępu i zarządzania fokusem ułatwień dostępu, możesz zamiast tego wdrożyć ExploreByTouchHelper. Zapewnia AccessibilityNodeProviderCompat i może być dołączony jako AccessibilityDelegateCompat widoku przez wywołanie setAccessibilityDelegate. Przykład znajdziesz w sekcji ExploreByTouchHelperActivity. ExploreByTouchHelper jest też używany przez widżety frameworka, takie jak CalendarView, za pomocą widoku podrzędnego SimpleMonthView.

Obsługa niestandardowych zdarzeń dotknięcia

Elementy sterujące widoku niestandardowego mogą wymagać niestandardowego zachowania zdarzeń dotknięcia, jak pokazano w poniższych przykładach.

Definiowanie działań opartych na kliknięciach

Jeśli widżet korzysta z interfejsu OnClickListener lub OnLongClickListener, system obsługuje działania ACTION_CLICKACTION_LONG_CLICK. Jeśli Twoja aplikacja korzysta z bardziej spersonalizowanego widżetu, który opiera się na interfejsie OnTouchListener, zdefiniuj niestandardowe moduły obsługi działań związanych z kliknięciem, które są dostępne w ramach ułatwień dostępu. Aby to zrobić, wywołaj metodę replaceAccessibilityAction() dla każdej czynności, jak pokazano 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

Kontrolka niestandardowa może używać onTouchEvent(MotionEvent)metody odbiornika do wykrywania zdarzeńACTION_DOWNACTION_UP oraz wywoływania specjalnego zdarzenia kliknięcia. Aby zachować zgodność z usługami ułatwień dostępu, kod obsługujący to niestandardowe zdarzenie kliknięcia musi:

  1. Wygeneruj odpowiedni AccessibilityEvent dla zinterpretowanego działania związanego z kliknięciem.
  2. Włącz usługi ułatwień dostępu, aby wykonywać niestandardowe kliknięcia dla użytkowników, którzy nie mogą korzystać z ekranu dotykowego.

Aby skutecznie spełnić te wymagania, kod musi zastąpić metodę performClick(), która musi wywoływać implementację superklasy tej metody, a następnie wykonywać działania wymagane przez zdarzenie kliknięcia. Gdy zostanie wykryte niestandardowe działanie kliknięcia, kod musi wywołać metodę performClick(). Poniższy przykład kodu pokazuje 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;
    }
}

Powyższy wzorzec pomaga zapewnić zgodność niestandardowego zdarzenia kliknięcia z usługami ułatwień dostępu, ponieważ używa metody performClick() do generowania zdarzenia ułatwień dostępu i zapewnia punkt wejścia dla usług ułatwień dostępu, aby działać w imieniu użytkownika wykonującego niestandardowe zdarzenie kliknięcia.