Search code examples
androidandroid-textinputlayouttalkbackandroid-textinputedittext

Android Talkback incorrect announcement on TextInputLayout


I have the following piece of code in my layout

<com.google.android.material.textfield.TextInputLayout
        android:id="@+id/tilPassword"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Password"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/tilUserName"
        app:passwordToggleEnabled="true">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/tiePassword"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:imeOptions="actionDone"
            android:inputType="textPassword"
            android:selectAllOnFocus="true"
            android:singleLine="true" />

    </com.google.android.material.textfield.TextInputLayout>

While navigating to TextInputLayout TalkBack announces : "password password edit box"

Desired announcement: "password edit box"

If I remove either android:hint="Password" or android:inputType="textPassword" it works as expected.

Notes about setting the hint

The hint should be set on TextInputLayout, rather than the TextInputEditText or EditText. If a hint is specified on the child EditText in XML, the TextInputLayout might still work correctly; TextInputLayout will use the EditText’s hint as its floating label. However, future calls to modify the hint will not update TextInputLayout’s hint. To avoid unintended behavior, call setHint() and getHint() on TextInputLayout, instead of on EditText.


Solution

  • By inspecting the source code of:

    TextInputLayout class

    https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/textfield/TextInputLayout.java

    one can find that:

    TextInputLayout Accessibility info is provided through the following public class:

    public static class AccessibilityDelegate extends AccessibilityDelegateCompat {
        private final TextInputLayout layout;
    
        public AccessibilityDelegate(TextInputLayout layout) {
            this.layout = layout;
        }
    
        @Override
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
            super.onInitializeAccessibilityNodeInfo(host, info);
            EditText editText = layout.getEditText();
            CharSequence text = (editText != null) ? editText.getText() : null;
            CharSequence hintText = layout.getHint();
            CharSequence errorText = layout.getError();
            CharSequence counterDesc = layout.getCounterOverflowDescription();
            boolean showingText = !TextUtils.isEmpty(text);
            boolean hasHint = !TextUtils.isEmpty(hintText);
            boolean showingError = !TextUtils.isEmpty(errorText);
            boolean contentInvalid = showingError || !TextUtils.isEmpty(counterDesc);
    
            if (showingText) {
                info.setText(text);
            } else if (hasHint) {
                info.setText(hintText);
            }
    
            if (hasHint) {
                info.setHintText(hintText);
                info.setShowingHintText(!showingText && hasHint);
            }
    
            if (contentInvalid) {
                info.setError(showingError ? errorText : counterDesc);
                info.setContentInvalid(true);
            }
        }
    }
    

    and that its is applied to TextInputLayout by calling the following public method:

    public void setTextInputAccessibilityDelegate(TextInputLayout.AccessibilityDelegate delegate) {
        if (editText != null) {
            ViewCompat.setAccessibilityDelegate(editText, delegate);
        }
    }
    

    so, one can extend:

    TextInputLayout.AccessibilityDelegate class and override onInitializeAccessibilityNodeInfo() to announce only what is needed. For example in your case, you can do the following:

    private class CustomTextInputLayoutAccessibilityDelegate extends TextInputLayout.AccessibilityDelegate{
    
        private final TextInputLayout layout;
    
    
        public CustomTextInputLayoutAccessibilityDelegate(TextInputLayout layout) {
            super(layout);
            this.layout = layout;
        }
    
        @Override
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
            super.onInitializeAccessibilityNodeInfo(host, info);
            EditText editText = layout.getEditText();
            CharSequence text = (editText != null) ? editText.getText() : null;
            CharSequence hintText = layout.getHint();
            CharSequence errorText = layout.getError();
            //CharSequence counterDesc = layout.getCounterOverflowDescription();
            boolean showingText = !TextUtils.isEmpty(text);
            boolean hasHint = !TextUtils.isEmpty(hintText);
            //boolean showingError = !TextUtils.isEmpty(errorText);
            //boolean contentInvalid = showingError || !TextUtils.isEmpty(counterDesc);
    
            if (showingText) {
                info.setText(text);
            } else if (hasHint) {
                info.setText("");
            }
    
            if (hasHint) {
                info.setHintText("");
                info.setShowingHintText(!showingText && hasHint);
            }
    
            //if (contentInvalid) {
            //    info.setError(showingError ? errorText : counterDesc);
            //    info.setContentInvalid(true);
            //}
        }
    }
    

    and then call:

    tilPassword.setTextInputAccessibilityDelegate(new CustomTextInputLayoutAccessibilityDelegate(tilPassword));