Accessible Tabbed Navigation in Android

Share on FacebookShare on LinkedInShare on Twitter

Deque U Best Practices Logo

This post was co-authored by Chris McMeeking and Melinda Kothbauer.

TabWidgets are a tool that can add great functionality to your mobile Android apps; however, they are inaccessible by default. WCAG 4.1.2 describes the need for all user interface components to have a name, role, and value to ensure that assistive technology users have the necessary information to navigate screens effectively. This is especially true for TabWidgets in Android because they are inaccessible otherwise. In order to make TabWidgets accessible, developers must make some programmatic customizations. This blog post will focus on:

  • Why TabWidgets are difficult
  • How to make TabWidgets accessible
  • Implementing accessible TabWidgets
  • Best practices for TabWidgets

This discussion references our open source app. Please get this running on your device and turn TalkBack on. You can also find our app on the Google Play Store, though the version released may be behind the version referenced in the open source repository. The Open Source app is the preferred reference. Before continuing find the "Tabbed Navigation" story, located in the main menu, and navigate to the broken tab. The following discussion will refer to the broken tab view as well as the fixed tab view.

Why are TabWidgets difficult?

When a tab is highlighted using TalkBack in Android, the default announcement is only the text (if any) in the current tab. It does not announce it's state - whether the tab is selected or not - nor where the current tab lies in the entire TabWidget (for example, tab # of #). It also does not announce it's role - that it is a tab. Try it on the "broken" page - all that is announced is "Cat," "Dog," or "Fish." A non-sighted user would not be able to tell that the highlighted element is a tab. There is nothing that indicates the highlighted view has any functionality whatsoever. To a TalkBack user, there is no difference between this TabWidget and a TextView. This behavior means default TabWidgets violate WCAG 2.0 success criteria.

How to make TabWidgets Accessible

In order to create an accessible application that still utilizes TabWidget functionality, Deque developers created a custom TabWidget layout, which extends a LinearLayout, to use instead of the default Android TabWidget. It implements the previously discussed name, role, and value attributes by overriding the default Android getContentDescription method, which is where TalkBack retrieves information to be announced, and implementing a method called findContentDescription to get the appropriate text if there is no content description.

getContentDescription Method

[code language="java"] @Override
public CharSequence getContentDescription() {

final TabWidget tabWidget = mTabHost.getTabWidget();
final View view = tabWidget.getChildTabViewAt(mTabHost.getCurrentTab());
final int tabCount = tabWidget.getTabCount();
int tabNumber;

for (tabNumber = 0; tabNumber < tabCount; tabNumber++) {
if (this == tabWidget.getChildTabViewAt(tabNumber)) break;
}

CharSequence contentDescription = findContentDescription(this);

if (view == this) {
contentDescription = contentDescription + ", selected";
}

contentDescription = contentDescription + ", tab " + (tabNumber + 1) + " of " + tabCount;

return contentDescription;
}
[/code]

Notice on lines 16 and 19 that we are overwriting the content description to include vital information (role and value). The findContentDescription method on line 13 is a custom method whose code can be found below.

findContentDescription Method

[code language="java"] private CharSequence findContentDescription(View view) {

if (view == null) return null;

CharSequence contentDescription = null;

if (TextView.class.isInstance(view)) {
TextView textView = (TextView)view;

contentDescription = textView.getText();
}

if (contentDescription != null) return contentDescription;

if (ViewGroup.class.isInstance(view)) {
ViewGroup viewGroup = (ViewGroup)view;

for (int i = 0; i < viewGroup.getChildCount(); i++) {
contentDescription = findContentDescription(viewGroup.getChildAt(i));

if (contentDescription != null) return contentDescription;
}
}

return null;
}
[/code]

Implementing accessible TabWidgets

Once you have created a custom TabWidget layout class, implementing it is relatively easy. Where you would normally use another layout for your tab widget in code, simply replace it with your custom TabWidget layout class. This may typically occur when setting up your tab host/ tab widget. See an example below.

Before

[code language="java"] mTabHost.addTab(mTabHost.newTabSpec("tab1").setContent(R.id.tab1).setIndicator(getTabIndicator(mTabHost.getContext(), R.string.aac_tab_nav_cat_tab_title)));

private View getTabIndicator(Context context, int title) {
View view = LayoutInflater.from(context).inflate(R.layout.tab_nav_story_tab_layout_broken, null);
TextView tv = (TextView) view.findViewById(R.id.aac_tab_nav_tab_title);
tv.setText(title);
return view;
}
[/code]

After

[code language="java"] mTabHost.addTab(mTabHost.newTabSpec("tab1").setContent(R.id.tab1).setIndicator(createTabIndicator(mTabHost.getContext(), R.string.aac_tab_nav_cat_tab_title)));

private View createTabIndicator(Context context, int title) {
TabLayout view = (TabLayout) LayoutInflater.from(context).inflate(R.layout.tab_nav_story_tab_layout_fixed, null);
view.setTabHost(mTabHost);
TextView tv = (TextView) view.findViewById(R.id.aac_tab_nav_tab_title);
tv.setText(title);
return view;
}
[/code]

Best Practice

By default, TabWidgets are not accessible in Android. To make them accessible, they need customization that adds their name, role, value, and state to ensure that TalkBack users have all the information they need to navigate the screen. This means that the content description of the tab needs to programmatically be updated to include the fact that the element is a tab, whether it's selected or not, which tab out of how many, and still include the text displayed in the tab. The Deque Way is not the only way to do this, but it is effective and relatively simple.

Tips

  • Don't use a default Android TabWidget - they are inaccessible. Instead, use a technique like Deque's and create a custom tab layout that appropriately assigns the necessary information dynamically to each tab.
  • Test your tab bars using TalkBack to ensure that it is announcing what you expect. Also test on a variety of devices to ensure you are not missing any device/version specific problems.

We hope you found this tutorial useful. Check out our Android application for more accessibility tutorials and demos!

Learn more...

Join our mailing list

About 

Chris McMeeking is a software engineer and architect at Deque Systems, leading development efforts on Deque’s native mobile accessibility analysis products. His journey in accessibility began through a project at the University of Michigan, The ASK Scanning Keyboard. This application won multiple awards including the $100,000 Intel Innovator’s Award, runner up at the Mobile World Congress, and the Student of Da Vinci award from the Multiple Sclerosis foundation. Chris is the lead developer behind the Android Analyzer, and an active member of the task force developing these new accessibility mobile standards.