Customizing TalkBack on a Custom Android View

The Problem

I was working on an accessibility project and the TalkBack experience for this view was terrible:

Screen Shot 2018-08-30 at 4.36.47 PM.png

Working with this view took two steps by the user, the TalkBack announcing each section individually:

  1. “Email note to customer” swipe gesture to move to next view
  2. “If disabled will add the note as private, OFF, switch…double-tap to toggle”

When the user double-tapped: “on” or “off”

original

The Goal

I wanted TalkBack to treat my view similar to the experience of interacting with the Mono audio settings option in Accessibility Settings:

mono_audio

The entire group of elements are treated as a widget:

“Mono audio, combine channels when playing audio, OFF, switch….double-tap to activate”

When the user double-tapped: “on” or “off”

Initial Attempts

My custom view wasn’t technically a custom view, it was simply a container of objects nested in a much larger view:
Screen Shot 2018-08-31 at 5.10.48 PM.png

I read through the Android Accessibility documentation and added android:focusable=true to the root LinearLayout container, but all that did was highlight the entire view on activate, which is better, but it still only announced the title: “Email note to customer”. I still had to swipe to get to the actual switch to hear “If disabled will add the note as private” and be given the option to toggle.

I added android:labelFor="@+id/view_switch" to the textview containing the summary and tried again.

first_pass

Now my TalkBack flow was:

  1. “Email note to customer” swipe gesture to move to next view
  2. “If disabled will add the note as private, OFF, switch, for email note to customer…double-tap to toggle”

Nope, that wasn’t the fix either. I decided the best plan was to just create a custom view component and handle the accessibility in a more direct manner.

Building the Custom View [GitHub]

I’ve always loved building custom views and Kotlin just makes them so much more elegant. I built a little sandbox project to test out the various changes. Included in this project is the original component – for easily comparing improvements, and the updated version, that I’ve lovingly labeled Talkbackified!

Screenshot_20180831-180658.png

Starting with the layout xml, I always use <merge/> to flatten my hierarchy as much as possible.

view_single_option_toggle.xml:

<?xml version="1.0" encoding="utf-8"?>
<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:focusable="true">

    <TextView
        android:id="@+id/switchSetting_title"
        style="@android:style/TextAppearance.DeviceDefault.Large"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clickable="false"
        android:textAlignment="viewStart"
        android:importantForAccessibility="no"
        tools:text="Email note to customer"/>

    <Switch
        android:id="@+id/switchSetting_switch"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textAlignment="viewStart"
        android:importantForAccessibility="no"
        android:clickable="false"
        tools:text="If disabled will add the note as private"/>
</merge>

Then I opened up the ability to customize the title, summary and checked state by adding some custom attributes:

attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SingleOptionToggleView">
        <attr name="switchTitle" format="reference|string"/>
        <attr name="switchSummary" format="reference|string"/>
        <attr name="switchChecked" format="boolean"/>
    </declare-styleable>
</resources>

Then create my custom class:

SingleOptionToggleView.kt

class SingleOptionToggleView @JvmOverloads constructor(ctx: Context, attrs: AttributeSet? = null)
    : LinearLayout(ctx, attrs), Checkable {
    init {
        orientation = LinearLayout.VERTICAL
        View.inflate(context, R.layout.view_single_option_toggle, this)

        if (attrs != null) {
            val a = context.obtainStyledAttributes(attrs, R.styleable.SingleOptionToggleView)
            try {
                val title = a.getString(R.styleable.SingleOptionToggleView_switchTitle).orEmpty()
                switchSetting_title.text = title
                switchSetting_switch.text =
                        a.getString(R.styleable.SingleOptionToggleView_switchSummary).orEmpty()
                switchSetting_switch.isChecked =
                        a.getBoolean(R.styleable.SingleOptionToggleView_switchChecked, false)

                switchSetting_switch.textOff = resources.getText(R.string.switch_disabled)
                switchSetting_switch.textOn = resources.getText(R.string.switch_enabled)
                setOnClickListener { toggle() }
            } finally {
                a.recycle()
            }
        }
    }
}

Using this view is as easy as plugging it into your main layout:

 android:focusable="true"
    android:clickable="true"
    android:importantForAccessibility="yes"
    android:background="#ffffff"
    app:switchTitle="@string/option_title"
    app:switchSummary="@string/option_summary"/>

Supporting Talkback

To get TalkBack working mostly the way I wanted it to (I’ll get to that later), I essentially had to override three methods:

//region Customized Accessibility events
override fun getAccessibilityClassName() = this::javaClass.name

override fun onInitializeAccessibilityEvent(event: AccessibilityEvent?) {
    event?.isChecked = isChecked
    super.onInitializeAccessibilityEvent(event)
}

override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) {
    info?.isCheckable = true
    info?.isChecked = isChecked
    super.onInitializeAccessibilityNodeInfo(info)
}
//endregion

Lessons Learned

  • Setting the child switch element to importantForAccessiblity="no" removed the action prompt of “double-tap to toggle”. But if you don’t set this then the focus will go to that switch next even if you set its focusable=false property!
    • Fix: Add importantForAccessibility="yes" to the root layout view. Since the root view is Checkable, and I’ve included the code to notify the Accessibility service of this in onInitializeAccessibilityInfo(...), the action prompt returns.
  • dispatchPopulateAccessibilityEvent() is only called if your view registers text content change events, but I’m still not 100% sure how all that works.
  • I was able to customize the initial checked-state announcement when the component first is activated by setting the textOn and textOff properties of my views switch.

Changes for another day

I tried to change the wording used when the user changes the state of my custom component, but eventually decided to leave that for another day. It defaults to “Checked” or “Not Checked”, when what I really wanted it to say was “Option on” or “Option off”.

Summary

The finished product works nicely with TalkBack. When activated, the component is announced with:

“Email note to customer, if disabled will add the note as private, option not checked”

When the state of the view is changed to on, it simply announces:

“Checked”

final

Screen Shot 2018-08-31 at 3.14.30 PM.png

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.