Android Nomad #49 - Apps for Accessibility
Designing accessibility for your android apps.
Before implementing accessibility in Android apps, it's essential to consider a range of factors to ensure your app is inclusive and usable by people with disabilities. Here's a breakdown:
1. Understand Accessibility Guidelines
- WCAG (Web Content Accessibility Guidelines): Familiarize yourself with these standards, especially the ones relevant to mobile apps.
- Android Accessibility Guidelines: Follow Google's accessibility best practices for Android.
2. Identify Your User Base
- Understand the needs of your target users, such as those with visual, auditory, physical, or cognitive impairments.
- Perform a usability study to understand gaps throughout the app.
3. Accessibility features on Android
These are some of the features that are available under Accessibility services.
Keyboard Access
Control android device via external keyboard and perform actions by pressing keys on the keyboard
Voice Access
Control android device via voice. This is especially useful for users who have difficulty using the touchscreen.
Talkback Access
It is built-in screen reader that allows you to navigate through apps and perform actions.
Switch Access
Control android device via external switches, buttons or braille displays. This is especially useful for people with a motor disability.
4. Plan for Accessibility from the Start
Accessibility should be a core design principle rather than an afterthought. This ensures better integration into your app's UI/UX.
Common accessibility concepts:
Appropriate labels for UI elements
Add meaningful
contentDescriptionto UI elements likeImageorIcon.
Proper color contrast
Maintain a high contrast ratio between text and background.
Avoid conveying information using color alone.
Sufficiently large touch targets
Ensure buttons and touchable elements have a minimum size of 48x48dp.
Dynamic Text Sizes
Support system font scaling (use
spfor text sizes)
Advanced accessibility concepts:
Custom accessibility actions
Ensure all functionality is accessible via keyboard or switch access.
Ensure proper navigation order and focus handling for all interactive elements.
Implementing custom descriptions
Testing for accessibility
Accessibility Testing Framework
Android Studio checks
Accessibility scanner
Espresso
Roboelectric
Manual testing with Talkback and Switch Access
4. How does Accessibility Services work in Android?
An Accessibility Service is a service that runs in the background and responds to the Accessibility Events like tap, focus, tap and hold, two finger swipe, swipe right, etc.
The screen/window content is arranged in a tree and each node in the tree is represented as an AccessibilityNodeInfo.
Each AccessibilityNodeInfo contains information like Name, Role, Value, and Action associated with that particular view.
AccessibilityNodeInfo are then used by this service to facilitate accessible feature whenever the devices turns on the accessible modes.
However, there are others that doesn’t directly rely on these services such as contrast, text size which are now part of display settings but falls into the category of accessibility.
In my previous blog, I covered basics of accessibility in Compose. Feel free to read them here.
5. Accessibility Role
An accessibility role helps users of assistive technologies to understand the purpose of elements on the screen.
Here are some accessibility roles in Jetpack Compose:
- Button: Indicates that an element is a button
- Checkbox: Indicates that an element is a checkbox with two states (checked/unchecked)
- DropdownList: Indicates that an element is a drop-down menu
- Image: Indicates that an element is an image
- RadioButton: Indicates that an element is a radio button
- Switch: Indicates that an element is a switch
- Tab: Indicates that an element is a tab that represents a single page of content
- ValuePicker: Indicates that an element is a value picker that should support accessibility scroll events
6. Semantic Markup
In compose, you can provide semantic markups via a Modifier to a UI element, this provision makes a UI element compatible with assistive features
contentDescriptionto provide a meaningful description for the screen reader for a particular UI element for e.g.Button, Icon, Box etc.In Jetpack Compose, you can use thecontentDescriptionortextproperties to set an accessibility label.ContentDescriptionis used for more visual elements, like icons and images.Textis used for text elements.
// set contentDescription
Box(modifier = Modifier.semantics {
contentDescription = "Appt"
}) {
// Box content...
}
// set text
Box(modifier = Modifier.semantics {
text = AnnotatedString("Appt")
}) {
// Box content...
}mergeDescendants -group all elements within this parent element and they will be focused and read out together.
liveRegion to add awareness to assisted screen readers of important changes in content through accessibility announcements, for e.g. a state change of an UI element
var isFavorite = remember { mutableStateOf(false) }
Icon(
modifier = Modifier
.wrapContentSize()
.semantics {
liveRegion = LiveRegionMode.Polite
}
.clickable {
// Handle on Click
},
imageVector = if (isFavorite.value) {
Icons.Rounded.Favorite
} else {
Icons.Rounded.FavoriteBorder
},
contentDescription = if (isFavorite.value) {
"Favorite"
} else {
"Not favorite"
}
)LiveRegionMode.Polite - waits for the speech announcement in progress to completeLiveRegionMode.Assertive - interrupts ongoing speech to immediately announce changes
// Merge all semantics of box elements
Box(modifier = Modifier
.semantics(mergeDescendants = true) { }
) {
// Box content ...
}customActions -provide a custom action for a UI element
Button(
onClick = { /* Your click handler */ },
modifier = Modifier
.wrapContentSize()
.semantics {
customActions = listOf(
// your custom action
CustomAccessibilityAction(label = "Add bookmark") {
// Bookmark logic
true
}
)
}
) {
Text("Bookmark button")
}Furthermore, there are several ways to override labels for default actions. For Composables that expose the onClick parameter, specify the label inside the semantics block modifier. The SemanticsActions object provides a list of all predefined accessibility actions.
// override label for button
val buttonClickHandler: () -> Unit = { /* Your click handler */ }
Button(
onClick = buttonClickHandler,
modifier = Modifier
.semantics {
onClick(label = "Add bookmark") {
buttonClickHandler.invoke()
true
}
}
) {
// Button content
}Managing FocusIn Jetpack Compose, you can use the contentDescription to hide an element (like an image) from assistive technologies by setting it to null.
// Set contentDescription to null
Image(
painter = /* your Painter */,
contentDescription = null,
)If Composable doesn't expose the contentDescription property, you can use the invisibleToUser property inside the semantics block modifier, to hide an element from assistive technologies.
// Make element invisible for assistive technologies
Text(
text = "",
modifier = Modifier.semantics {
invisibleToUser()
}
)If, on the contrary, you want to make a certain element focusable, use the focusable modifier. By design, some Composables are focusable, such as a Button or a Composable with the clickable modifier attached to it.
// Make element focusable
Box(modifier = Modifier.focusable()) {
// Box content...
}In Jetpack Compose, you can use the onFocusChanged modifier to listen for focus changes. It contains a property such, as isFocused, to check if composable currently has focus.
Button(
onClick = { /* Your click handling */ },
modifier = Modifier
.onFocusChanged {
if (it.isFocused) {
// logic in case view is focused
}
}
) {
// Button content ...
}panelTitle
A modal overlays the screen with additional content. Modals are usually indicated visually, e.g. by throwing a shadow on the content below it. Users of assistive technologies also need to know when content is part of a modal.
In Jetpack Compose, to set a title for an accessibility pane, you can use paneTitle inside semantics block modifier.
It's recommended to set the pane title for high-level layouts, such as Scaffold.
Scaffold(modifier = Modifier.semantics {
paneTitle = "Appt pane"
}) { paddingValues ->
// Scaffold content...
}traversalGroup -This will let assistive technologies know that this section is grouped and should be traversed, before moving on to a next section. Then addtraversalIndexto elements in this group to fix any issues with the focus order.In Jetpack Compose, you can use thetraversalIndexto alter the focus order of the screen. ThetraversalIndexwill give assistive technologies an explicit order of traversing. When a section of a screen is read out in an incorrect order, start by addingisTraversalGroupto the parentColumn,RoworBox.It is possible to useisTraversalGroupandtraversalIndexon the same element.
Box(modifier = Modifier.semantics {
isTraversalGroup = true
traversalIndex = -1f
}) {
// Box content...
}stateDescription -An accessibility state helps users of assistive technologies to understand the state of elements on the screen.
Box(
modifier = Modifier.semantics {
// Custom state
stateDescription = "Expanded"
// Selected
selected = true
}
)7. Keyboard Navigation
By adjusting the keyboard order, you can provide a great experience for users that control their device using a hardware keyboard.
@Composable
fun CustomFocusTraversal() {
// Create set of references for each Composable
val (first, second, third, fourth) = remember { FocusRequester.createRefs() }
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// First Button
Button(
onClick = { },
modifier = Modifier
.focusRequester(first)
.focusProperties {
down = second
}
) {
Text("Button 1")
}
Spacer(modifier = Modifier.height(16.dp))
// Second Button
Button(
onClick = { },
modifier = Modifier
.focusRequester(second)
.focusProperties {
up = first
down = third
}
) {
Text("Button 2")
}
Spacer(modifier = Modifier.height(16.dp))
// Third Button
Button(
onClick = { },
modifier = Modifier
.focusRequester(third)
.focusProperties {
up = second
down = fourth
}
) {
Text("Button 3")
}
Spacer(modifier = Modifier.height(16.dp))
// Fourth Button
Button(
onClick = { },
modifier = Modifier
.focusRequester(fourth)
.focusProperties {
up = third
right = first
}
) {
Text("Button 4")
}
}
// Set initial focus
LaunchedEffect(Unit) {
first.requestFocus()
}
}
8. Localization and Language
- Support multiple languages.
- Ensure text directions (LTR or RTL) are handled appropriately.
Assistive technologies, such as the screen reader, use the locale for the pronunciation of utterances. It is important to explicitly set a locale for your app. An incorrect locale leads to unclear pronunciation. Also, setting a locale can help with displaying characters correctly.
val context = LocalContext.current
val localizedContext = remember {
val locales = LocaleList.forLanguageTags("nl-NL")
val configuration = context.resources.configuration
configuration.setLocales(locales)
context.createConfigurationContext(configuration)
}
val localizedString = localizedContext.resources.getString(R.string.appt)
Text(text = localizedString)By considering these aspects, you can create a more inclusive experience, ensuring your app is accessible to all users.
