Custom view accessibility support on Android TV

While many Android TV apps are built with Android native components, it's equally important to give careful consideration to accessibility of third party frameworks or components, in particular where custom views are used.

Such components interfacing directly with OpenGL or Canvas may not work well with accessibility services such as Talkback and Switch Access.

Consider some of the following issues that might occur with Talkback switched on:

  • The accessibility focus (a green rectangle) disappears in your app.
  • The accessibility focus selected the boundary of the whole screen.
  • The accessibility focus cannot be moved.
  • Four direction DPAD keys have no effect, even if your code is handling them.

If you observe any of these issues in your app, you may need to ensure that your app exposes its AccessibilityNodeInfo tree to the accessibility services.

D-pad events are consumed by accessibility services

The root cause of this issue is that key events are consumed by accessibility services.

Dpad events consumption

As illustrated in the figure above, when Talkback is switched on, D-pad events will not be passed to the D-pad handler defined by developer. Instead, accessibility services receive the key events in order to move the accessibility focus. Because custom Android components do not by default expose information to accessibility services about their position on the screen, accessibility services cannot move the accessibility focus to highlight them.

Other accessibility services are similarly affected: D-pad events may also be consumed when using Switch Access.

In conclusion, because D-pad events are submitted to accessibility services, and that service doesn't known where UI components are in a custom view, you must implement AccessibilityNodeInfo in order for your app to forward the different key events correctly.

Exposing information to accessibility services

In order to provide accessibility services with sufficient information about the location and description of custom views, AccessibilityNodeInfo should be implemented to expose details for each component. To define the logical relationship of views for accessibility services to be able to manage focus, implement ExploreByTouchHelper and set it using ViewCompat.setAccessibilityDelegate(View, AccessibilityDelegateCompat) for custom views.

When implementing ExploreByTouchHelper, override its four abstract methods:

Kotlin

// Return the virtual view ID whose view is covered by the input point (x, y).
protected fun getVirtualViewAt(x: Float, y: Float): Int

// Fill the virtual view ID list into the input parameter virtualViewIds.
protected fun getVisibleVirtualViews(virtualViewIds: List<Int>)

// For the view whose virtualViewId is the input virtualViewId, populate the
// accessibility node information into the AccessibilityNodeInfoCompat parameter.
protected fun onPopulateNodeForVirtualView(virtualViewId: Int, @NonNull node: AccessibilityNodeInfoCompat)

// Set the accessibility handling when perform action.
protected fun onPerformActionForVirtualView(virtualViewId: Int, action: Int, @Nullable arguments: Bundle): Boolean

Java

// Return the virtual view ID whose view is covered by the input point (x, y).
protected int getVirtualViewAt(float x, float y)

// Fill the virtual view ID list into the input parameter virtualViewIds.
protected void getVisibleVirtualViews(List<Integer> virtualViewIds)

// For the view whose virtualViewId is the input virtualViewId, populate the
// accessibility node information into the AccessibilityNodeInfoCompat parameter.
protected void onPopulateNodeForVirtualView(int virtualViewId, @NonNull AccessibilityNodeInfoCompat node)

// Set the accessibility handling when perform action.
protected boolean onPerformActionForVirtualView(int virtualViewId, int action, @Nullable Bundle arguments)

For more details, refer to this Google I/O session on enabling blind and low-vision accessibility, or read more about populating accessibility events.

Essentials

  1. Required: AccessibilityNodeInfo.getBoundsInScreen() must define the position of the component.

  2. Required: AccessibilityNodeInfo.setVisibleToUser() should reflect the visibility of the component.

  3. Required: AccessibilityNodeInfo.getContentDescription() should specify the content description for the Talkback to announce.

  4. AccessibilityNodeInfo.setClassName() should be specified to allow services distinguish the component type.

  5. When implementing performAction(), the action should also be reflected by a corresponding AccessibilityEvent.

  6. In order to implement more action types, such as ACTION_CLICK, invoke AccessibilityNodeInfo.addAction(ACTION_CLICK) with corresponding logic in performAction().

  7. When applicable, reflect the component state for setFocusable(), setClickable(), setScrollable() and similar methods.

  8. Review the documentation for AccessibilityNodeInfo to identify other ways in which accessibility services can better interact with your components.

Sample

Consult the custom view accessibility sample for Android TV with best practices for adding accessibility support for apps using custom views.