Chapters

Hide chapters

Android Accessibility by Tutorials

Second Edition · Android 12 · Kotlin 1.6 · Android Studio Chipmunk

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section I: Android Accessibility by Tutorials

Section 1: 13 chapters
Show chapters Hide chapters

Section II: Appendix

Section 2: 1 chapter
Show chapters Hide chapters

7. Operable — Navigating the Screen
Written by Victoria Gonda

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

If you’ve worked through this book chapter by chapter, then you’ve learned a lot about what comprises a perceivable app. You might be surprised to learn that it’s not enough for your app to be perceivable. It also needs to be operable, which is defined by the WCAG as:

2. Operable: User interface components and navigation must be operable.

This definition means that users should be able to perform actions and navigate your app, whether they use fingers, voice, screen readers or something else. Every user should have the same choices for actions and views they can reach.

In this chapter, you’ll focus on making Taco Tuesday navigable with accessibility services, and therefore more operable.

Traversing Using a Keyboard

There are multiple ways to navigate an Android device with a keyboard. For example, in Chapter 3, “Testing & Tools”, you learned about using TalkBack as a keyboard. You can also connect a keyboard to most mobile devices and navigate with keystrokes. For testing, you can create emulators that make use of your computer’s keyboard.

To navigate with a keyboard, you use the Tab and Arrow keys. When you’re testing how well keyboard navigation works, you want to make sure that everything is reachable, elements are navigated in a logical order, and you don’t get trapped in one part of the screen.

WCAG’s guideline for keyboards is straightforward:

Guideline 2.1 Keyboard Accessible: Make all functionality available from a keyboard.

If you build on native components, then keyboard navigation should work pretty well. You won’t need to change a lot, and you can focus on other operability issues and fine-tuning the experience.

Adjusting Navigation Order

If you find that you need to change some element’s ordering to improve keyboard navigation, you can use a couple of XML layout attributes. Here’s the first:

android:nextFocusForward="@+id/editText1"
android:nextFocusUp="@+id/editText1"
android:nextFocusDown="@+id/editText2"
android:nextFocusLeft="@+id/editText3"
android:nextFocusRight="@+id/editText4"

Navigating Your App

This chapter focuses on how to allow users who use accessibility tools to navigate your app. WCAG’s guideline is logical but broad:

Distinguishing List Items

As you know from earlier in the book, content descriptions are important. They also must be unique so that a user knows where they are on the screen and which item an action might affect.

Multiple items have the same description.
Rofsurci ubemn qoge hgu kado pifbhiwliun.

Improving Content Descriptions

Open TryItRecipesRecyclerViewAdapter.kt. In ViewHolder, find bind(). Here is where you’ll add all your content descriptions.

binding.itemRecipeMade.contentDescription =
 itemView.context.getString(
 R.string.try_it_description_made_recipe, recipe.name)

binding.itemRecipeDetails.contentDescription =
 itemView.context.getString(
 R.string.try_it_description_details_recipe, recipe.name)
TalkBack output with unique descriptions.
RuhtWozx iajlip caky ojemuo makcbofpeojq.

Keeping List Item Focus

Focus items have similar requirements. They need to flow in a logical order, and it must be apparent to the user where they are on the screen. It’s also acceptable, if not advisable, to skip duplicated content.

TalkBack focus before and after checking the box.
BikfCowm vugit libiye ejj olbor tcolnerv xno yob.

Resolving the List Item Focus Bug

First, disable the item animator:

itemAnimator = null
init {
 setHasStableIds(true)
}
override fun getItemId(position: Int): Long {
 return getItem(position).id
}
TalkBack focus before and after checking the box.
NegtQoqw huxux loqata edp urnep bzuwqicz bfu xuc.

Managing Links

Links are common in apps, especially when displaying user-generated content. Because of this, there are criteria for addressing links. Here’s one of them:

Exploring Links in Taco Tuesday

Take a look at a detailed view of a recipe. There’s a bit of informational text below the description that says Recipe from TacoFancy.

Recipe from TacoFancy.
Xuvize lceg GesoSigrr.

TalkBack links menu.
VargLezm ligqr vibo.

Improving the Experience Around Links

One option is to find the links in the text and then extract and display the link details. You see this in many apps. For example, Twitter shows link previews.

Tweet with link preview.
Preub xejd yodn jgoyuey.

Implementing TtsSpan

You’ll use TtsSpan as a custom Span for your markdown links.

.usePlugin(object : AbstractMarkwonPlugin() {
 override fun configureTheme(builder: MarkwonTheme.Builder) {
 builder.linkColor(ContextCompat.getColor(requireContext(),
  R.color.colorPrimary))
 }
})
.usePlugin(object : AbstractMarkwonPlugin() {
 override fun configureSpansFactory(
  builder: MarkwonSpansFactory.Builder
 ) {
 super.configureSpansFactory(
  builder.setFactory(Link::class.java,
   object : LinkSpanFactory() {
    override fun getSpans(
     configuration: MarkwonConfiguration,
     props: RenderProps
    ): Any {

    }
   })
 )
 }
})
val href = CoreProps.LINK_DESTINATION.require(props)
val uri = Uri.parse(href)
return arrayOf<Any>(

)
LinkSpan(configuration.theme(), href,
 configuration.linkResolver()),
ForegroundColorSpan(ContextCompat.getColor(requireContext(),
 R.color.colorPrimary)),
TtsSpan.ElectronicBuilder()
 .setPort(uri.port)
 .setDomain(uri.host)
 .setPath(uri.path)
 .setQueryString(uri.query)
 .build()

Handling Gestures

Support for gestures gives an app a layer of polish — allowing your users to swipe or pinch to perform actions can bring delight.

Adding Long Press to Discard

The first option is a long press. While still not very discoverable, this option preserves your pristine UI.

// 1
binding.itemRecipeTitle.setOnLongClickListener {
 // 2
 MaterialAlertDialogBuilder(it.context)
  .setTitle(R.string.try_it_discard_confirm_title)
  .setMessage(it.context.getString(
    R.string.try_it_discard_confirm_message, recipe.name))
  // 3
  .setPositiveButton(
    R.string.try_it_discard_confirm_discard) { _, _ ->
     onDiscardRecipe(recipe)
    }
  // 4
  .setNegativeButton(
    R.string.try_it_discard_confirm_cancel) { _, _ -> }
  .show()
 true
}
Dialog to confirm discarding a recipe.
Fookub qe cavtowk mofdobrunn i zikuxa.

Describing Actions

Now attempt the same action with TalkBack turned on. You’ll hear “Double-tap and hold to long-press”.

Double-tap and hold to long press.
Neogfo-ban ihz saqh du lirc ydisj.

class DeleteRecipeAccessibilityDelegate(
 private val recipeName: String
) : AccessibilityDelegateCompat() {

}
override fun onInitializeAccessibilityNodeInfo(
 host: View,
 info: AccessibilityNodeInfoCompat
) {
 // 1
 super.onInitializeAccessibilityNodeInfo(host, info)
 // 2
 val longClick =
  AccessibilityNodeInfoCompat.AccessibilityActionCompat(
   AccessibilityNodeInfo.ACTION_LONG_CLICK,
   host.context.getString(
    R.string.try_it_description_discard_recipe,
    recipeName))
 // 3
 info.addAction(longClick)
}
ViewCompat.setAccessibilityDelegate(binding.itemRecipeTitle,
 DeleteRecipeAccessibilityDelegate(recipe.name))
Double-tap and hold to discard Moroccan Lamb Tacos.
Ruazwo-kav apy demv ma cassuxs Tezadqoq Lewd Muhag.

Adding a Discard Button

The other option you’ll implement is adding a one-tap discard action to the list item. For this exercise, you’ll add one adjacent to the button to view a full recipe.

<ImageButton
 android:id="@+id/item_recipe_discard"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 app:iconGravity="end"
 android:contentDescription="@string/shared_discard"
 android:src="@drawable/ic_baseline_thumb_down_24"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintEnd_toStartOf="@+id/item_recipe_details"
 app:layout_constraintStart_toStartOf="parent"
 app:layout_constraintTop_toBottomOf="@id/item_recipe_rating"
 />
app:layout_constraintStart_toEndOf="@id/item_recipe_discard"
app:layout_constraintStart_toStartOf="parent"
binding.itemRecipeDiscard.setOnClickListener {
 onDiscardRecipe(recipe)
}
Screenshot of discard button.
Lpwaunfwak on gipsetd sukjoj.

Considering Touch Targets

Have you ever run across a button or link that’s hard to tap unless you zoom in? This is an example of a touch target issue. WCAG says this about touch targets:

Consider making this clickable item larger.
Nezqoher bubuvr bnuz xvetnokya otej xephiv.

Fixing Touch Targets

You’ll fix these up while making them look a bit nicer by making these targets into MaterialButtons.

android:src="@drawable/ic_baseline_thumb_down_24"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_margin="@dimen/space_normal"
android:text="No thanks"
app:icon="@drawable/ic_baseline_thumb_down_24"
app:iconPadding="@dimen/drawable_padding"
<com.google.android.material.button.MaterialButton
 android:id="@+id/discover_button_discard"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:background="?colorBackgroundFloating"
 android:clickable="true"
 android:focusable="true"
 android:contentDescription="@string/shared_discard"
 style="@style/Widget.MaterialComponents.Button.TextButton"
 android:layout_margin="@dimen/space_normal"
 android:text="No thanks"
 app:icon="@drawable/ic_baseline_thumb_down_24"
 app:iconPadding="@dimen/drawable_padding"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintEnd_toStartOf="@id/discover_button_try"
 app:layout_constraintHorizontal_chainStyle="packed"
 app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
 android:id="@+id/discover_button_try"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:background="?colorBackgroundFloating"
 android:clickable="true"
 android:focusable="true"
 android:contentDescription="@string/shared_try_it"
 style="@style/Widget.MaterialComponents.Button.TextButton"
 android:layout_margin="@dimen/space_normal"
 android:text="Save for later"
 app:icon="@drawable/ic_baseline_thumb_up_24"
 app:iconPadding="@dimen/drawable_padding"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintEnd_toEndOf="parent"
 app:layout_constraintStart_toEndOf="@id/discover_button_discard" />
New buttons on the discover screen.
Pid dajtiqg ev vci yolyocum pfmeej.

<com.google.android.material.button.MaterialButton
 android:id="@+id/item_recipe_discard"
 style="@style/Widget.MaterialComponents.Button.TextButton"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 app:iconGravity="end"
 android:contentDescription="@string/shared_discard"
 android:textColor="?colorOnPrimary"
 app:icon="@drawable/ic_baseline_thumb_down_24"
 app:iconTint="?colorOnPrimary"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintEnd_toStartOf="@+id/item_recipe_details"
 app:layout_constraintStart_toStartOf="parent"
 app:layout_constraintTop_toBottomOf="@id/item_recipe_rating"
 />

<com.google.android.material.button.MaterialButton
 android:id="@+id/item_recipe_details"
 style="@style/Widget.MaterialComponents.Button.TextButton"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:contentDescription="@string/shared_details"
 android:textColor="?colorOnPrimary"
 app:icon="@drawable/ic_baseline_view_24"
 app:iconTint="?colorOnPrimary"
 app:layout_constraintBottom_toBottomOf="parent"
 app:layout_constraintEnd_toEndOf="parent"
 app:layout_constraintStart_toEndOf="@id/item_recipe_discard"
 app:layout_constraintTop_toBottomOf="@id/item_recipe_rating"
 />
New buttons on list items.
Kib bijnixr oc tiww ifobt.

Targeting Links

While inline links are exempt from touch target size guidance, there’s a specific case for following the guidfeline to make a link more clickable: anytime you have a sentence with a single link. For example, “By tapping ‘Continue’ you agree to our Privacy Policy”, where Privacy Policy is the linked text.

Recipe from TacoFancy.
Pifaye svij ReqiXobtr.

Recipe from TacoFancy
recipeDetailCreditText.setOnClickListener {
 startActivity(Intent(
   Intent.ACTION_VIEW,
   Uri.parse("https://github.com/sinker/tacofancy")))
}
recipeDetailCreditText.movementMethod =
 LinkMovementMethod.getInstance()

Challenges

Challenge 1: Add a unique description to the discard button

In this chapter, you gave list item elements unique content descriptions. But you also added a view to the list item that could be unclear: a discard button.

Key Points

  • Operability is a crucial part of achieving accessibility.
  • Accessibility tools help you identify operability issues within an app’s navigation.
  • All elements of a given view must be reachable via a keyboard interface.
  • Content descriptions for different list items should be unique so that the user can differentiate between similar items.
  • Focus ordering should flow in a logical pattern.
  • When performing operations on list items, make sure a screen reader can keep its focus on the correct element.
  • Make link text clear and descriptive, and use TtsSpan when applicable.
  • Actions triggered by gestures should also be reachable via a single tap.
  • Touch targets should be at least 48dp by 48dp.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now