Chapters

Hide chapters

Android Accessibility by Tutorials

First Edition · Android 11 · Kotlin 1.4 · AS 4.1

Before You Begin

Section 0: 2 chapters
Show chapters Hide chapters

Section I: Android Accessibility by Tutorials

Section 1: 13 chapters
Show chapters Hide chapters

Section II: Appendix

Section 2: 1 chapter
Show chapters Hide chapters

10. Robust
Written by Tori Gonda

People use their devices in many different ways, so you need to make sure your app is compatible with accessibility services.

While much of the work happens automatically or is trivial to implement, you need to put in effort for custom views. That’s the main focus of this chapter.

You’ve explored perceivable, operable and understandable. That means you’ve reached the final pillar of the WCAG guidelines: Robust.

Robust: Content must be robust enough that it can be interpreted by a wide variety of user agents, including assistive technologies.

A robust app is one that people can access in various ways, including with different assistive technologies, such as screen readers.

Android does a lot of the heavy lifting by providing components with built-in support. And it provides an interface for you to leverage. In this chapter, you’ll learn how to use these built-in tools to improve your apps by providing more information about views.

Success Criterion 4.1.2 Name, Role, Value: For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies.

Level A

This criterion may sound daunting. And you’re not wrong. However, this chapter will give you the insights and practice you need.

You can either work on the starter project for this chapter or continue with the project you’ve used earlier in the book.

Relying on system views

The simplest way to satisfy the success criterion is by using the views that Android provides. They typically include everything — or almost everything — you need to inform accessibility services about a view’s role and content.

In other words, if you can use a system view instead of creating a custom view, do that! You can customize it; for example, if you need a custom button, you extend the Button rather than starting from scratch with a View.

In Taco Tuesday, you can take advantage of a system view to improve the recipe details screen. There’s a Made it checkbox that is currently two different views: the label and the check box.

Made recipe checkbox.
Made recipe checkbox.

Turn on TalkBack, run the app and observe how this view behaves with the screen reader.

You must highlight them separately. There’s no indication that the checkbox belongs to the “Made recipe” label.

Made recipe TalkBack reading.
Made recipe TalkBack reading.

You can improve this by including the text of the label as part of the checkbox.

Open fragment_recipe_detail.xml. Delete the label:

<TextView
 android:id="@+id/recipe_detail_made_it_label"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="@string/recipe_detail_made_recipe" />

You’re deleting the old view because you’ll combine these views,

You also need to delete references to this label in the Kotlin code:

  1. Open RecipeDetailFragment.kt.
  2. Delete the recipeDetailMadeItLabel.visibility = View.VISIBLE line in showEditableFields().
  3. Delete the recipeDetailMadeItLabel.visibility = View.GONE line in hideEditableFields().

Then, in fragment_recipe_detail.xml, add the text of the label to the CheckBox with ID recipe_detail_made_it:

android:text="@string/recipe_detail_made_recipe"

Now, the CheckBox owns the label and can inform the accessibility services about the associated label.

Build and run. Use TalkBack again and notice that the checkbox is correctly labeled.

Made recipe checkbox with updated TalkBack.
Made recipe checkbox with updated TalkBack.

To make stylistic changes to the CheckBox or any other view, modify or extend the CheckBox itself. Don’t create something new. Using existing views allows you to leverage Android’s built-in support assistive technologies.

Indicating a view’s role

Although the system’s components will support most accessibility services without any intervention, you may need to make adjustments to achieve the desired experience. In many cases, you can use an accessibility delegate to make these modifications.

In Chapter 7, “Operable — Navigating the Screen”, you added an AccessibilityDelegateCompat to create a custom action name. To modify other accessibility attributes of the view, you can employ the same pattern. Try it out for yourself!

To prepare, add this item to strings.xml:

<string name="banner_role">banner</string>

You’ll use this in the next step. Don’t worry about translations for now.

Experimenting with delegates

Next, you’ll experiment with a delegate for the banner that displays across the top of the screen.

  1. Open the Project tab on the left side of Android Studio.
  2. Create a new class named BannerAccessibilityDelegate.kt in the com.raywenderlich.android.tacotuesday package.
  3. Open the new class and set it to extend AccessibilityDelegateCompat, like this:
class BannerAccessibilityDelegate : AccessibilityDelegateCompat()

As you did before, override onInitializeAccessibilityNodeInfo() in BannerAccessibilityDelegate.kt, and add all neccessary imports:

override fun onInitializeAccessibilityNodeInfo(
  host: View?,
  info: AccessibilityNodeInfoCompat?
) {
 // 1
 super.onInitializeAccessibilityNodeInfo(host, info)
 // 2
 info?.roleDescription =
   host?.context?.getString(R.string.banner_role)
}

You’re implementing two actions in this method:

  1. Calling the super method so the superclass can handle its accessibility node info.
  2. Setting the role description of the view to banner.

Note: These actions are meant to be an example. Generally, you won’t need to set the role.

You may need to set the role if a custom view’s role is incorrect. For example, when you treat a component like a button, it isn’t actually a button.

Use your new delegate: In MainActivity.kt, add this to the bottom of onCreate():

ViewCompat.setAccessibilityDelegate(
  binding.mainBanner,
  BannerAccessibilityDelegate()
)

Build and run. Use TalkBack to see how it announces your role.

TalkBack reading banner role.
TalkBack reading banner role.

The AccessibilityNodeInfoCompat has many properties. You can use them to define if a view is clickable, checkable and more. Please take a few moments to experiment with the other properties to see how they interact with TalkBack.

If some of this seems familiar, remember, you learned how to use it to add custom actions in Chapter 7, “Operable – Navigating the Screen”.

There are other methods you can override on AccessibilityDelegateCompat by using methods such as onPopulateAccessibilityEvent() and onInitializeAccessibilityEvent(). With these, you can define changes based on events; for example, if the state changes from unchecked to checked.

You should review Android documentation for guidance about working on a custom view that is actionable or has a changing state — this book does not go into that use case.

The rest of this chapter will be a bit harder — you’ll focus on a more complicated example.

Note: Learn more about how to use an accessibility delegate at https://developer.android.com/guide/topics/ui/accessibility/custom-views#populate-events and see what you can set on an AccessibilityNodeInfoCompat at https://developer.android.com/reference/androidx/core/view/accessibility/AccessibilityNodeInfoCompat.

Building custom views

Custom views can become incredibly complex with different touch areas, actions and behaviors. You need to communicate this complexity to the accessibility services. To make your task a little trickier, documentation around these use cases is a bit…sparse.

Taco Tuesday has a custom view with multiple touch areas: CustomRatingBar, which is the nacho and spiciness rating bars on the details screen.

Nacho and spice rating bars.
Nacho and spice rating bars.

Try using TalkBack on this view. Spoiler alert: It needs improvement. You can’t select individual items, discover the current rating or select a new rating.

Rating bar TalkBack selection.
Rating bar TalkBack selection.

As it stands, the accessibility service thinks this is a single view. It doesn’t know that you can touch the different ratings or do other things.

Virtual views are treated as their own views from a perception and operation standpoint. They are one means to inform accessibility services about different touch areas.

Using ExploreByTouchHelper

If you research how to create these virtual views, you’ll find many options. This chapter will teach you how to use an ExploreByTouchHelper, a type of accessibility delegate that can help you define touch areas.

Open CustomRatingBar.kt. Create an inner class for your touch helper delegate:

inner class CustomRatingBarExploreByTouchHelper(host: View) :
  ExploreByTouchHelper(host) {
}

You’re using an inner class so that you can access properties and methods on the CustomRatingBar.

This new inner class is where you’ll do much of the work to inform the accessibility services about the state and events of the virtual views. When you’re done, it will show a compiler error until you override the required methods.

Defining virtual views

The first things you’ll define are:

  1. How many virtual views exist.
  2. Where they’re located.
  3. What properties they have.

Virtual view count

CustomRatingBar defines the locations of these views in the rectangles list. You’ll use this list occasionally to get information about the virtual views.

Override getVisibleVirtualViews() to define how many views there are with:

override fun getVisibleVirtualViews(
  virtualViewIds: MutableList<Int>?
) {
 rectangles.forEachIndexed { index, _ ->
  virtualViewIds?.add(index)
 }
}

The parameter for this method is a list of virtual IDs. You add all your virtual IDs to this list. For simplicity, this method uses the index of the views for the IDs.

Virtual view location

Next, you need to define where these views are. Add this code to CustomRatingBarExploreByTouchHelper:

override fun getVirtualViewAt(x: Float, y: Float): Int {
 // 1
 val index = findRatingAtPoint(x, y)
 // 2
 return if (index == INVALID_VALUE) INVALID_ID else index
}

With getVirtualViewAt(), you inform the accessibility services which virtual view was acted on at a point. By section:

  1. Look up which virtual view at this location uses an existing method in CustomRatingBar. The index is the ID.
  2. Return index of the ID if it’s valid. Otherwise, return INVALID_ID. One reason for the latter to happen is that an area of your custom view is not a virtual view.

Virtual view properties

Next, you need to share the information about the view. Add this for onPopulateNodeForVirtualView():

override fun onPopulateNodeForVirtualView(
  virtualViewId: Int,
  node: AccessibilityNodeInfoCompat
) {
 // 1
 node.text = context.getString(
   R.string.custom_rating_bar_description,
   label,
   virtualViewId + 1
 )
 // 2
 node.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK)
 // 3
 node.setBoundsInParent(rectangles[virtualViewId])
}

Here you have the same AccessibilityNodeInfoCompat class you used when creating delegates before. Using that node, you:

  1. Set the text of the virtual view. If the view doesn’t have text, you set this to be identical to the content description. Remember that you’re using the index as the ID, so the first item is index 0, the second is index 1, etc. You adjust the index value with virtualViewId + 1 to get which rating item it is.
  2. Add a click action to inform the services that this item is clickable.
  3. Define the bounds for this view using the saved rectangle. This is also where you can set the state for events, a checked box or some error. Now the accessibility services know what to read when they reach this virtual view.

Note: node.setBoundsInParent(rectangles[virtualViewId]) line will show as deprecated. Don’t worry about it. At the time of writing, there’s a bug that causes the app to crash if you don’t include it.

Performing actions

Finally, when someone initiates a click action, regardless of if it was with a physical tap or through an accessibility service, you need to add logic to handle it correctly.

Override this last method in CustomRatingBarExploreByTouchHelper:

override fun onPerformActionForVirtualView(
  virtualViewId: Int,
  action: Int,
  arguments: Bundle?
): Boolean {
 when (action) {
  AccessibilityNodeInfoCompat.ACTION_CLICK -> {
   onSelected(virtualViewId)
   return true
  }
 }
 return false
}

This view handles click actions, and because there is already a helper method for this action, you’re:

  • Checking for a click action on the selected ID.
  • Calling onSelected() when there is a click action.

Linking the view and delegate

Now you’re ready to hook up the delegate that defines your virtual views to your custom view.

Add this in CustomRatingBar:

private val exploreByTouchHelper = CustomRatingBarExploreByTouchHelper(this)

You’ve just initialized a property for your touch helper delegate and can access the delegate in all the places you need it.

In the bottom of the init block, add this line:

ViewCompat.setAccessibilityDelegate(this, exploreByTouchHelper)

Now your delegate is added to the view.

Finally, add these methods to CustomRatingBar:

override fun dispatchHoverEvent(event: MotionEvent?): Boolean {
 return (event?.let {
  exploreByTouchHelper.dispatchHoverEvent(it)
 } ?: run { false }
   || super.dispatchHoverEvent(event))
}

override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
 return (event?.let {
  exploreByTouchHelper.dispatchKeyEvent(it)
 } ?: run { false }
   || super.dispatchKeyEvent(event))
}

override fun onFocusChanged(
  gainFocus: Boolean,
  direction: Int,
  previouslyFocusedRect: Rect?
) {
 super.onFocusChanged(gainFocus, direction,
   previouslyFocusedRect)
 exploreByTouchHelper.onFocusChanged(gainFocus, direction,
   previouslyFocusedRect)
}

This large chunk forwards the required methods to the accessibility delegate.

Phew! You made it. Build and run. Use TalkBack to navigate through and select a rating.

TalkBack nacho rating selection.
TalkBack nacho rating selection.

Improving the state

When using TalkBack, you can’t discern a recipe’s current rating or know when it changes. This diminishes the experience, so you’ll set the content description to the current rating when a user rates a recipe.

You’ll accomplish this by adding to a custom setter on the rating variable.

Create a string resource for this description, then add this to strings.xml:

<string name="current_rating_description">Current rating is %d</string>

Don’t worry about adding translations for this exercise.

Then, append the content description to the custom rating setter underneath invalidate() in CustomRatingBar.kt:

contentDescription = context.getString(R.string.current_rating_description, value)

The content description now includes the current rating.

You can further improve the experience by making sure that TalkBack announces when the rating has changed. You learned a trick about setting the accessibility live region in Chapter 9, “Understandable”, which you’ll use here.

Add this line to the bottom of the init block:

accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_POLITE

When the content description changes, it will be announced.

Build and run. Try to change the rating using TalkBack, and listen for it to announce the change.

Current rating announced.
Current rating announced.

This view is now MUCH more accessible. Great work!

Seeing service limitations

While working through this book, you’ve probably noticed that different devices and Android builds support different accessibility services. The differences go deeper than that.

You cannot rely on accessibility services behaving similarly on different devices. Manufacturers configure these services differently, and users can modify settings that change the services’ behaviors.

There are some compat classes to help you handle different Android versions, and in many cases, you can rely on these. They’re helpful when you’re working with APIs that are only supported on newer versions.

This book has not covered accessibilityTraversalAfter and accessibilityTraversalBefore because they’re known to have inconsistent and unreliable behavior on some devices.

Because of these constraints, you should avoid trying to make a view behave a specific way when you’re testing. You should assume that the user is familiar with how these services work on their device. When testing, you should focus on ensuring you’re giving the accessibility services accurate information and creating a consistent experience.

Android is continually evolving and improving its accessibility technology, and each version of Android comes with a promise for new and better services. So while it is a challenge to keep up, you can look forward to improved services with each new release.

Challenges

Challenge 1: Fix the rating bar

The rating bar is editable when on the details screen and not editable when in the list view.

That’s because something is missing from your CustomRatingBar; specifically, there is an editable flag that is not respected when using TalkBack.

Your challenge is to make some changes so that this flag is respected.

Hint: You don’t need the accessibility delegate if the view is not editable.

Compare your results with the challenge project in the resources for this chapter. If you get stuck, that same project will show you the solution.

Key points

  • A robust app is one that integrates with accessibility services.
  • System views are the most reliable way to support accessibility across devices.
  • An accessibility delegate is one of the ways to communicate details to accessibility services.
  • Use ExploreByTouchHelper to create and manage virtual views when a custom view has multiple touch targets.
  • Accessibility services often behave differently on different devices.
  • Android is making continuous improvements to accessibility services.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.