08/15/2023

Tech Notes

If you have ever used Android and Talkback, I’m sure you have heard the “Double tap to activate” instruction before. It is an indication that a control is clickable. This is fine for things like buttons, which should be clickable, but the issue is that this announcement happens when there is a click event tied to a certain element. It doesn’t matter if the click does anything or not, or if the element is announced as a valid control such as “button”. The only thing that matters for the “double tap to activate” instruction to be announced is that the “clickable” property is set to true.

The Issue

Now, this can cause some confusion because sometimes an element will be indicated as clickable, but will not actually do anything. The lack of a role such as “button” also makes it difficult to determine what will actually happen if the control is clicked. If it is a link, it might open your selected browser app, and if it's a button, it may perform an action within the current app.

There are some arguments that say “double tap to activate” is enough of an announcement to indicate that a control is actionable. I would say that is true to an extent, but I don’t believe that it is enough to satisfy the 4.1.2 Name, Role, Value WCAG success criteria, which states:

“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.”

Since a role is not conveyed to the user, this scenario will fail the success criteria. In my mind, this is no different from a screen reader like NVDA simply announcing that an element is “clickable” with no role announced. You may also notice if you have an older version of Talkback that if you change Talkback to the “Controls” granularity movement method, a control that does not have a role does not appear in this type of movement:

You can watch a "No Role" Talkback demo video here.

The Solution

There are a few ways to ensure that a control has a proper role associated with it. One way is to use or extend from native Android widgets such as a Button. Another way is to override the accessibility class name of the view using AccessibilityNodeInfo.setClassName(java.lang.CharSequence)). When overriding the class name, it is important to use a class that Talkback actually supports. Documentation is not great, but a list of classes can be found in the Talkback source code in the Role.java file. And even digging through the source code, it's not very clear how it's done. The easiest way that I know how to do it is to provide the class name of the role that you want the control to be identified as. For example, “info.setClassName(Button.class.getClassName())”.

 View customButton = binding.getRoot().findViewById(R.id.clickableTextView);

// Create a custom Accessibility Delegate

View.AccessibilityDelegate buttonDelegate = new View.AccessibilityDelegate() { @Override

public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {

    super.onInitializeAccessibilityNodeInfo(view, info);

    // Set the class name to a button

    info.setClassName(Button.class.getName());

}
};

// Set the Accessibility Delegate

customButton.setAccessibilityDelegate(buttonDelegate);

The next way is to use the Accessibility Node Info compatibility class to define a role description. Keep in mind that this description accepts any value and Talkback will announce that value. Although I wouldn’t recommend it, if you wanted to define your control as a “sandwich”, Talkback would announce that value as the role for the control. But the idea would be to define this as a “button”. If your app supports multiple languages, you may need to address translation issues if using this method.

 View.AccessibilityDelegate buttonDelegate = new View.AccessibilityDelegate() {

@Override

public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {

    super.onInitializeAccessibilityNodeInfo(view, info);

    // create the Accessibility Node Info compatibility object

    AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info);

    // Set the role description

    infoCompat.setRoleDescription("button");

}
};

// Set the Accessibility Delegate

customButton.setAccessibilityDelegate(buttonDelegate);

You can watch Talkback "Button Role" demo video here.

Common Questions

Links are handled differently in Android. A link is a ClickableSpan, which can create some confusion because most text elements within an Android app are “Spannable” strings. Spannable strings are pieces of text that can have styles or attributes applied to them. One attribute being a ClickableSpan among many others. There are many ways to make a span clickable, the most common way that I see is to use a URLSpan. Since most pieces of text in Android are Spannable, you can technically have inside of a button where the button text is acting as a link although valid use cases for this particular scenario are pretty slim.

 textView.setText(Html.fromHtml("American Foundation for the Blind", Html.FROM_HTML_MODE_COMPACT));

// Be sure to set the movement method so the link is focusable when using a keyboard

textView.setMovementMethod(LinkMovementMethod.getInstance());

How Do I Create an iOS Adjustable Equivalent in Android?

If you are familiar with iOS and VoiceOver, you have probably used an adjustable control that you can swipe up or down to change the value. Android and Talkback have similar functionality in something like a volume control. You can add this functionality if you have the right combination of roles and accessibility actions. For starters, the control needs to be identified as a SeekBar. This is done by setting the AccessibilityNodeInfo class name of the view to “SeekBar.class.getName()”. That will allow us to add the AccessibilityActions of ACTION_SCROLL_FORWARD and ACTION_SCROLL_BACKWARD for swipe up and down respectively. This is done using AccessibilityNodeInfo.addAction()). After adding the actions, we have to capture those events by overriding the performAccessibilityAction) method.

 View.AccessibilityDelegate sliderDelegate = new View.AccessibilityDelegate() {

@Override

public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {

  super.onInitializeAccessibilityNodeInfo(view, info);

  info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);

  info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);

  AccessibilityNodeInfoCompat infoCompat = AccessibilityNodeInfoCompat.wrap(info);

  infoCompat.setClassName(SeekBar.class.getName());

  // You can define a range value if your component calls for it 

  infoCompat.setRangeInfo(AccessibilityNodeInfoCompat.RangeInfoCompat.obtain(RANGE_TYPE_PERCENT, 0.0f, 50.0f, 100.0f));
}

@Override

public boolean performAccessibilityAction(View host, int action, Bundle args) {

  boolean response = super.performAccessibilityAction(host, action, args);

  if(action == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD.getId()) {

      // decrease value

      return true;

  }

  if(action == AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.getId()) {

      // increase value

      return true;

  }

  return response;
}

};

slider.setAccessibilityDelegate(sliderDelegate);

How Do I Create a Logical Group?

Grouping for accessibility purposes in Android is done using CollectionInfo and CollectionItemInfo within the AccessibilityNodeInfo for the view. The CollectionInfo should be defined for the outer container. A Collection for Talkback can be thought of as a grid or a list and it will be announced as such depending on how it is defined. A group or list (note that it will always be identified as a list by Talkback), is just a grid that has a single column. So you define the parameters of the CollectionInfo as one column with however many rows or items are contained within the group. You also have to define the CollectionItemInfo for each item in the group. These are simple properties but can be a little confusing if you are unfamiliar with it, but as long as you keep in mind that a list is just a single column grid, it will make sense.

  • rowIndex: This is where the particular item appears in the list. It is zero-based so if it is the first item in the list, this value would be 0.
  • rowSpan: If this were a grid, how many rows would this span? Since we’re just making a list, it would span 1 row. So the value would just be 1.
  • columnIndex: For just a list, this will always be 0 since there is only a single column.
  • columnSpan: The same thinking as a rowSpan. Since it is just a list, it would be 1.
  • heading: Is this a column or row heading? Since this is just a list, the answer would be no, so this is usually false.
 View.AccessibilityDelegate parentViewDelegate = new View.AccessibilityDelegate() {

@Override

public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {

    super.onInitializeAccessibilityNodeInfo(view, info);

    AccessibilityNodeInfo.CollectionInfo collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(

            childViews.length,

            1,

            false);

    info.setCollectionInfo(collectionInfo);

    info.setImportantForAccessibility(true);

}
};

parentView.setAcceessibilityDelegate(parentViewDelegate);

int index = 0;

for (View childView: childViews) {

final int i = index;

View.AccessibilityDelegate childViewDelegate = new View.AccessibilityDelegate() {

    @Override

    public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfo info) {

        super.onInitializeAccessibilityNodeInfo(view, info);

        AccessibilityNodeInfo.CollectionItemInfo collectionItemInfo =

                AccessibilityNodeInfo.CollectionItemInfo.obtain(

                        i,

                        1,

                        0,

                        1,

                        false);

        info.setCollectionItemInfo(collectionItemInfo);

        info.setImportantForAccessibility(true);

    }

};

childView.setAccessibilityDelegate(childViewDelegate);

index++;
}

Android Accessibility Inspector

Talkback has a method of displaying debug information using the Node Tree Debugging. This method is documented in various articles, but Google appears to have removed information related to this feature from their documentation resources. There is an archive of the documentation that can be dug up here. This debug information can be difficult to read and interpret, so I put together a tool that performs a similar function in an easier to read format. That tool can be found on my Github page.

The Android Accessibility Inspector will display the accessibility node tree of the active window and provide various pieces of information such as the effective role associated with the element, the text, content description, and other properties and events tied to the element. This tool uses the same APIs and information that Talkback uses to navigate the screen and is a big help in debugging issues and determining if a button is actually a button or if “button” is just included in the content description.

About AFB Talent Lab

The AFB Talent Lab ​​aims to meet the accessibility needs of the tech industry – and millions of people living with disabilities – through a unique combination of hands-on training, mentorship, and consulting services, created and developed by our own digital inclusion experts. To learn more about our internship and apprenticeship programs or our client services, please visit our website at www.afb.org/talentlab.