I have some screens in my app where TalkBack does not infer the correct reading order. According to the documentation, I can use android:accessibilityTraversalAfter
and friends to alter the reading order. But it does not work for me for elements within a focusable ViewGroup
that should be read together.
The entire layout looks like that:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:accessibilityTraversalBefore="@id/before"
android:focusable="true"
tools:context=".MainActivity">
<TextView
android:id="@+id/before"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:accessibilityTraversalAfter="@id/before"
android:text="Before"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="@+id/after"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:accessibilityTraversalBefore="@id/after"
android:text="After"
app:layout_constraintBottom_toTopOf="@+id/before"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
It renders After
in the middle of the screen, Before
at the bottom. I want TalkBack to treat the entire screen as a single contiguous element, hence I set android:focusable
to true
. By default, TalkBack reads: "After, before". But I want it to read "Before, after". Although I've added android:accessibilityTraversalBefore
and android:accessibilityTraversalAfter
, it still reads "After, before". That's the output of the Node Tree Debugging:
TreeDebug: (-2147455381)429.FrameLayout:(0, 0 - 1080, 1920):A
TreeDebug: (30189)429.TextView:(42, 101 - 397, 172):TEXT{My Application}:A:supportsTextLocation
TreeDebug: (31150)429.ViewGroup:(0, 210 - 1080, 1794):Fa:focusable:accessibilityFocused
TreeDebug: (33072)429.TextView:(499, 951 - 581, 1002):TEXT{After}:A:supportsTextLocation
TreeDebug: (32111)429.TextView:(485, 1743 - 595, 1794):TEXT{Before}:A:supportsTextLocation
What am I doing wrong?
Just for completeness: minSdkVersion
is 26, targetSdkVersion
is 29.
I've dug into the problem and found out following: accessibilityTraversalBefore
and accessibilityTraversalAfter
flags in deed take effect, but only for what they are intended for - they are intended for Accessibility Service app (e.g. Talkback). In other words if you remove focusable
attribute from root layout you'll see that navigation is correct.
But those flags do not affect as to how AccessibilityNode
is constructed for root ViewGroup
. As can be seen in sources of ViewGroup#onInitializeAccessibilityNodeInfoInternal()
the actual text construction logic does not regard how children construct their navigation using upper mentioned flags.
In order to solve the problem I've removed excessive flags from layout xml as such:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
tools:context=".MainActivity">
<TextView
android:id="@+id/before"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:text="Before"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/after"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:text="After"
app:layout_constraintBottom_toTopOf="@+id/before"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
And inside the host activity/fragment:
val root = findViewById<ViewGroup>(R.id.root)
ViewCompat.setAccessibilityDelegate(root, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfoCompat?
) {
val stringBuilder = StringBuilder()
root.children.forEach { view ->
val label = if (view is TextView) view.text else ""
stringBuilder.append("$label, ")
}
info?.text = stringBuilder.toString()
super.onInitializeAccessibilityNodeInfo(host, info)
}
})
This will result in the desired outcome: Talkback will pronounce "Before, After".
Unfortunately, this is an error prone code, meaning that if you restructure the view hierarchy in a way, that order of children gets swapped then this node text construction logic will become broken. Nevertheless, I couldn't come up with a better solution and cannot see it's possible to instruct parent to regard child ordering flags (based on sources).