Search code examples
androidandroid-layoutandroid-viewtalkbackaccessibility

Change reading order of elements within a focusable ViewGroup


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.


Solution

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