Accessible Custom Components
There are multiple things to consider when creating a custom component. The first is: should you even be making a custom component? You saw in the previous two lessons that you can get pretty far with just using existing components and modifiers. And you should! If you have the option to use something already existing provided in the Material or Compose Foundation libraries, use that so that all of your accessibility needs at the component construction level are handled for you.
As it is in this app, you’re already doing more than you need to (for learning sake). To drive home this point, simplify the favorite toggle feature.
Using Built-in Components
Back in DetailScreen.kt, look for the Icon with the toggleable modifier. Replace it with the following code:
IconToggleButton(
checked = cat.isFavorite,
onCheckedChange = { detailViewModel.toggleFavorite() }
) {
Icon(
imageVector = if (cat.isFavorite) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
contentDescription = stringResource(id = R.string.detail_favorite_label)
)
}
By using the IconToggleButton, you don’t need to give a second thought about the accessibility implementation because it’s handled for you. Build and run the app and there should be no accessibility regressions when observing the change with TalkBack.
Material components allow many ways to modify their appearance, so you can go a long way with them. Always check this library and the modifications it allows before considering building from scratch.
But sometimes, the existing libraries don’t offer what you need. What do you do then?
Using Canvas or Layout
Say you’re creating something really custom, such as a graph to display data. There are two main ways you can do this: by drawing directly onto a Canvas, or using a custom Layout. What option is more accessible? Time for an example to find out.
If you’re working from your Lesson 2 project, copy over SleepBarGraph.kt and SleepChart.kt from the materials for this project if you haven’t already.
Each of these files includes an implementation of a simple graph to display some of the nap data. One, SleepChart.kt, uses Canvas; the other, SleepBarChart.kt uses Layout. You can take a look at the implementations if you like, but those details aren’t terribly important right now.
Below the Column with the age and notes (remember, the ones you merged in the last lesson), add the following code:
Text(
text = stringResource(id = R.string.graph_title_box_layout),
style = MaterialTheme.typography.h6,
modifier = Modifier
.padding(bottom = 8.dp)
.semantics { heading() }
)
SleepBarGraph(
naps = cat.naps, modifier = Modifier
.fillMaxWidth()
.height(32.dp)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.graph_title_canvas),
style = MaterialTheme.typography.h6,
modifier = Modifier
.padding(bottom = 8.dp)
.semantics { heading() }
)
SleepChart(
naps = cat.naps,
modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(MaterialTheme.colors.onPrimary)
)
This adds both versions of the graph, along with some headers to make it clearer which is which. Build and run the app so you can take a closer look.
Right now, they seem exactly the same, both with and without TalkBack.
In fact, TalkBack completely skips both of the graphs! That’s not a very efficient way to convey information. Time to fix that.
Let’s start with the Canvas implementation in SleepChart.kt. In this composable, the shapes for the graph are drawn directly. How can we improve this? Well, you can add a content description. You know how to do that already!
But what if this were a graph that included a whole lot of information? That wouldn’t work very well in that scenario. With that much data, a person using accessibility services would likely find it helpful to walk through parts of the graph one at a time.
Unfortunately, there’s not currently a good way to do that with the Canvas implementation. You just get one node for the tree, the Canvas itself. Worse than an empty food bowl.
What about the Layout option? Is that any better? Yes! If you take a look at the top of SleepBarGraph, the Layout has a content block where it calls the Box composable for each nap. That means there’s a node in the tree you can annotate for each bit of data!
Update that content block to look like the following:
content = {
naps.forEach { nap ->
val formatter = DateTimeFormatter.ofPattern("h:mm a")
val description = stringResource(
id = R.string.nap_content_description,
nap.start.format(formatter),
nap.end.format(formatter)
)
Box(modifier = Modifier
.background(MaterialTheme.colors.primary)
.semantics {
contentDescription = description
})
}
}
The above code adds a contentDescription to each Box representing a data point, rather than the full graph. Build and run the app and observe the changes using TalkBack.
Imagine how powerful this can be! You can add any sort of action or semantic to each meaningful shape on the graph. Imagine how you could use the collectionInfo you learned about in a more complex graph or heading() to improve navigation. It’s purrfectly clear that the Layout option is best for this scenario.
Custom Actions
Speaking of adding actions to nodes, this lesson would not be complete without talking about custom actions. Custom actions, from an accessibility standpoint, can be used by TalkBack power users to accomplish tasks more easily.
Imagine there was a swipe action on the Home Screen to favorite a cat. Building that is out of scope for this lesson, but you can picture what it might look like. It’d be a bit tricky for a person using TalkBack to perform, since their swipe is used for navigation,but they can open the detail screen to favorite instead, so it’s okay, right?
Well, regardless of whether accessibility tools are being used, we can provide a similar experience for all users by adding a custom action.
Go to HomeScreen.kt and find the clickable Card. Add the following Modifier to the chain:
.semantics {
customActions = listOf(
CustomAccessibilityAction(label = favoriteActionLabel, action = {
onFavoriteClicked()
true
})
)
}
This uses the seemingly all-knowing semantics modifier to add a CustomAccessibilityAction that toggles whether the cat is a favorite.
Since you need text for the label, add this above the Card:
val favoriteActionLabel = if (cat.isFavorite) {
stringResource(id = R.string.action_label_unfavorite)
} else {
stringResource(id = R.string.action_label_favorite)
}
Build and run the app. To view and use your custom action, you’ll need to learn a new TalkBack gesture.
With one of the list items in focus using TalkBack, swipe down and to the right or up and to the right to open the TalkBack context menu.
Navigate to Actions and double-tap to select it. That will bring up a sub-menu that shows all of the actions you can choose from. That’s where you can find and select your new Favorite action to perform!
Great job adding some pretty complicated accessibility features!