Levon Petrosyan
Levon Petrosyan

Reputation: 9655

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.

Upvotes: 5

Views: 4414

Answers (4)

Jossy Paul
Jossy Paul

Reputation: 1287

We can try giving a custom role description for the AccessibilityNode. We can check if we are showing masked password using info.isPassword property. Then build roleDescription accordingly.

class MyAccessibilityDelegate(val layout: TextInputLayout) : TextInputLayout.AccessibilityDelegate(layout) {

    override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
        super.onInitializeAccessibilityNodeInfo(host, info)
        val editText = layout.editText
        val content = editText?.text
        info.text = content
        info.roleDescription = if(!info.isPassword) "password edit text" else "edit text"
        info.hintText = ""
        info.isShowingHintText = false
        editText?.setSelection(content?.length ?: 0)
    }
}

Upvotes: 0

Brainnovo
Brainnovo

Reputation: 1829

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

Upvotes: 4

Projit Roy
Projit Roy

Reputation: 19

It works for me. Use hint text in EditText hint.

<android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/_10"
        android:layout_marginLeft="@dimen/_10"
        android:layout_marginRight="@dimen/_10"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent">
        <EditText
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:hint="Address Line 1"/>
     </android.support.design.widget.TextInputLayout>

Upvotes: 0

Richard Dapice
Richard Dapice

Reputation: 885

I think it is reading your TextInputLayout hint. The layout shouldn't need a hint, move that to your TextInputEditText. You can also use the field importantForAccessability="false" on your top layout on the layout if for some reason you need a hint there.

<android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <android.support.design.widget.TextInputEditText
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="Password" />

        </android.support.design.widget.TextInputLayout>

Upvotes: 0

Related Questions