Making Custom Views on Android Accessible

In this post, we’ll add support for Google TalkBack (and other Android accessibility services) to a MovieRatings custom view

Ataul Munim
Google Developer Experts
4 min readAug 14, 2020

--

I’ve been working very slowly on an Android app in my spare time. It’s a client for Letterboxd (like Goodreads but for movies) and I’m currently replicating a feature that’s available on the website: you can see the user rating distribution for every movie.

The rating distribution for Marriage Story (2019) tends towards more favorable reviews // https://letterboxd.com/film/marriage-story-2019/

The API gives us everything that we need to build it ourselves, so I created a quick prototype, drawing the chart directly on a canvas, then tested it with Google TalkBack, an Android accessibility service for blind and visually impaired users.

The screen capture below shows navigation with TalkBack enabled (the white circle shows the “move to next element” gesture).

Screen capture showing that the text elements are focusable using Google TalkBack, but the ratings distribution view is not.

It’s being ignored. Accessibility services rely on information being exposed about each view through instances of AccessibilityNodeInfo. Since we’re drawing directly to a canvas, the metadata for this view is empty; for TalkBack users, the chart doesn’t exist.

From the point of view of an AccessibilityService a window's content is presented as a tree of accessibility node infos, which may or may not map one-to-one to the view hierarchy. In other words, a custom view is free to report itself as a tree of accessibility node info.

Instead of recreating the chart using a separate view for each interval, let’s create virtual view hierarchy. We can keep our canvas drawing and make it accessible too.

To make this view accessible, there’s two things we need to do:

  1. describe each interval
  2. indicate where each interval is

Luckily, we can use ExploreByTouchHelper, a class from the Jetpack Customview library, which does all of the difficult bits for us.

Using ExploreByTouchHelper

ExploreByTouchHelper is an abstract class that extends AccessibilityDelegateCompat.

We create our helper as an inner class of the custom view so that we can access the same properties that the onDraw() function uses, then we can set it as a delegate in the init block.

There are four functions that we need to implement and they sound complicated, but let’s go through them one-by-one.

  1. fun getVisibleVirtualViews(virtualViewIds: MutableList<Int>)

We need to populate the list with the IDs of all of the visible virtual views (the intervals in the chart). In our case, all intervals are always visible, so we’ll return a list of all IDs.

The IDs are really simple. We’re just using the index of the interval.

2. fun getVirtualViewAt(x: float, y: float): Int

For this function, we need to return the ID of the virtual view that’s under the x, y position, or ExploreByTouchHelper.HOST_ID if there’s no item at those coordinates.

We’re bending the rules a little here by ignoring the y value to increase the touch area. Even if the user is touching the space above the bar (but still within the RatingsView) we’re reporting that the bar is being touched:

3. fun onPopulateNodeForVirtualView(virtualViewId: Int, node: AccessibilityNodeInfoCompat)

This is where we provide all the metadata for our virtual view.

We need to set the content description (or text, if it’s presented visually) and set the bounds in parent.

The bounds in the parent should match the logic in the onDraw() function.

4. fun onPerformActionForVirtualView(virtualViewId: Int, action: Int, arguments: Bundle?): Boolean

Since we didn’t add any accessibility actions in step 3, this should never be called. We can just return false here.

And that’s it! Testing it with TalkBack again, now we can use the “move to next element” gesture to select each interval and the description for each one is read aloud.

Screen capture showing intervals in the ratings distribution view being focused by Google TalkBack

Where’s my touch exploration?

Another way that Google TalkBack users navigate through content is by using Explore-by-Touch. Notice how the text views gain accessibility focus when the user touches them, but the intervals don’t:

Screen capture showing that the intervals (virtual views) are resistant to focus when using Explore-by-Touch

To fix this, we need to override a function in our custom view:

and finally, everything works as expected:

Screen capture showing that both text views and each interval is accessible to TalkBack and Explore-by-Touch

Should probably update the content description to something useful though.

Try it out!

I found the official documentation quite difficult to find and use, but there are excellent samples in the Jetpack repository and also in the MDC for Android library.

You can see the code for this demo here — it took less than 50 lines of code, thanks to this helper class!

Let me know how you found this article on Twitter or by leaving a comment below.

--

--

Ataul Munim
Google Developer Experts

Android Developer Relations Engineer, focusing on Wear OS.