1

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.

azizbekian
  • 60,783
  • 13
  • 169
  • 249
aha
  • 3,702
  • 3
  • 38
  • 47
  • Can you verify, that you see this happening in a blank project with that layout? Or are you able to reproduce the issue only within your app/your setup? – azizbekian Sep 28 '19 at 10:03
  • This layout is pulled from an empty sample app created with AS 3.5 using the blank template. Activity is empty except `onCreate()` that does a `super()` call and `setContentView()`. – aha Sep 28 '19 at 10:14

1 Answers1

5

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).

azizbekian
  • 60,783
  • 13
  • 169
  • 249
  • Thanks for your in-depth investigation. `focusable` is primarily there to help with keyboard navigation. That it has an effect on TalkBack is a bonus considering that `screenReaderFocusable` is only available on API 28. But even with `screenReaderFocusable` alone it doesn't read `before` before `after`. Therefore I find the first paragraph confusing. What's the purpose of those attributes if they don't affect the `AccessibilityNode` of the `ViewGroup`? But thanks a lot for confirming that I have to construct the text hierarchy myself. – aha Oct 02 '19 at 13:43