Android Nomad #49 - Apps for Accessibility

Designing accessibility for your android apps.

Android Nomad #49 - Apps for Accessibility

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 contentDescription to UI elements like Image or Icon.

    • 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 sp for 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.

Android Nomad - #33 Accessibility
As mobile developers, it's our responsibility to ensure that our apps are accessible to all users, regardless of their abilities. With the introduction of Jetpack Compose, Google's modern toolkit for building native Android UI, we have powerful new tools at our disposal to create more accessible applications. In this post, we'll explore how Jetpack Comp…

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

  • contentDescription to 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 the contentDescription or text properties to set an accessibility label. ContentDescription is used for more visual elements, like icons and images. Text is 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 add traversalIndex to elements in this group to fix any issues with the focus order.In Jetpack Compose, you can use the traversalIndex to alter the focus order of the screen. The traversalIndex will give assistive technologies an explicit order of traversing. When a section of a screen is read out in an incorrect order, start by adding isTraversalGroup to the parent Column, Row or Box.It is possible to use isTraversalGroup and traversalIndex on 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.


Resources:

  1. Accessibility for Compose
  2. Principles for improving app’s accessibility
  3. Testing for accessibility
  4. Appt

Subscribe to Sid Pillai

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe