39

The TextInputLayout contains an EditText which in turn receives the input from the user. With TextInputLayout introduced with the Android Design Support Library we're supposed to set the error to the TextInputLayout holding the EditText rather than the EditText itself. When writing the UI will be focused on only the EditText and not the whole TextInputLayout which can lead to the keyboard covering the error. In the following GIF notice that the user has to remove the keyboard first to see the error message. This in combination with setting IME actions to move on using the keyboard leads to really confusing results.

example error

Layout xml code:

<android.support.design.widget.TextInputLayout
    android:id="@+id/uid_text_input_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:errorEnabled="true"
    android:layout_marginTop="8dp">

    <EditText
        android:id="@+id/uid_edit_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:singleLine="true"
        android:hint="Cardnumber"
        android:imeOptions="actionDone"/>

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

Java code setting the error to the TextInputLayout:

uidTextInputLayout.setError("Incorrect cardnumber");

How can I make sure the error message is visible without the user acting to see it? Is it possible to move the focus?

arekolek
  • 9,128
  • 3
  • 58
  • 79
Franzaine
  • 393
  • 3
  • 8

7 Answers7

9

Update: looks like this might have been fixed in 1.2.0-alpha03 version of the library.


To make sure the error message is visible without the user acting to see it, I subclassed TextInputLayout and placed it inside a ScrollView. This lets me scroll down if needed to reveal the error message, on every occasion the error message is set. There are no changes needed in the activity/fragment class that uses it.

enter image description here

import androidx.core.view.postDelayed

/**
 * [TextInputLayout] subclass that handles error messages properly.
 */
class SmartTextInputLayout @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextInputLayout(context, attrs, defStyleAttr) {

    private val scrollView by lazy(LazyThreadSafetyMode.NONE) {
        findParentOfType<ScrollView>() ?: findParentOfType<NestedScrollView>()
    }

    private fun scrollIfNeeded() {
        // Wait a bit (like 10 frames) for other UI changes to happen
        scrollView?.postDelayed(160) {
            scrollView?.scrollDownTo(this)
        }
    }

    override fun setError(value: CharSequence?) {
        val changed = error != value

        super.setError(value)

        // work around https://stackoverflow.com/q/34242902/1916449
        if (value == null) isErrorEnabled = false

        // work around https://stackoverflow.com/q/31047449/1916449
        if (changed) scrollIfNeeded()
    }
}

Here are the helper methods:

/**
 * Find the closest ancestor of the given type.
 */
inline fun <reified T> View.findParentOfType(): T? {
    var p = parent
    while (p != null && p !is T) p = p.parent
    return p as T?
}

/**
 * Scroll down the minimum needed amount to show [descendant] in full. More
 * precisely, reveal its bottom.
 */
fun ViewGroup.scrollDownTo(descendant: View) {
    // Could use smoothScrollBy, but it sometimes over-scrolled a lot
    howFarDownIs(descendant)?.let { scrollBy(0, it) }
}

/**
 * Calculate how many pixels below the visible portion of this [ViewGroup] is the
 * bottom of [descendant].
 *
 * In other words, how much you need to scroll down, to make [descendant]'s bottom
 * visible.
 */
fun ViewGroup.howFarDownIs(descendant: View): Int? {
    val bottom = Rect().also {
        // See https://stackoverflow.com/a/36740277/1916449
        descendant.getDrawingRect(it)
        offsetDescendantRectToMyCoords(descendant, it)
    }.bottom
    return (bottom - height - scrollY).takeIf { it > 0 }
}

I also fixed TextInputLayout.setError() leaves empty space after clearing the error in the same class.

arekolek
  • 9,128
  • 3
  • 58
  • 79
  • hi @arekolek what about if the textinputlayout is in a recyclerview, will this work? presumably yes since a recyclerview contains a scrollview, but wanted to check – davy307 Oct 22 '20 at 16:23
4

This is actually a known issue at Google.

https://issuetracker.google.com/issues/37051832

Their proposed solution is to create a custom TextInputEditText class


class MyTextInputEditText : TextInputEditText {
    @JvmOverloads
    constructor(
        context: Context,
        attrs: AttributeSet? = null,
        defStyleAttr: Int = android.R.attr.editTextStyle
    ) : super(context, attrs, defStyleAttr) {
    }

    private val parentRect = Rect()

    override fun getFocusedRect(rect: Rect?) {
        super.getFocusedRect(rect)
        rect?.let {
            getMyParent().getFocusedRect(parentRect)
            rect.bottom = parentRect.bottom
        }
    }

    override fun getGlobalVisibleRect(rect: Rect?, globalOffset: Point?): Boolean {
        val result = super.getGlobalVisibleRect(rect, globalOffset)
        rect?.let {
            getMyParent().getGlobalVisibleRect(parentRect, globalOffset)
            rect.bottom = parentRect.bottom
        }
        return result
    }

    override fun requestRectangleOnScreen(rect: Rect?): Boolean {
        val result = super.requestRectangleOnScreen(rect)
        val parent = getMyParent()
        // 10 is a random magic number to define a rectangle height.
        parentRect.set(0, parent.height - 10, parent.right, parent.height)
        parent.requestRectangleOnScreen(parentRect, true /*immediate*/)
        return result;
    }

    private fun getMyParent(): View {
        var myParent: ViewParent? = parent;
        while (!(myParent is TextInputLayout) && myParent != null) {
            myParent = myParent.parent
        }
        return if (myParent == null) this else myParent as View
    }
}```
elFonzo
  • 68
  • 5
3

@user2221404 answer did not work for me so I changed the getMyParent() method to what is shown:

class CustomTextInputEditText @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = android.R.attr.editTextStyle
) : TextInputEditText(context, attrs, defStyleAttr) {

private val parentRect = Rect()


override fun getFocusedRect(rect: Rect?) {
    super.getFocusedRect(rect)
    rect?.let {
        getTextInputLayout()?.getFocusedRect(parentRect)
        rect.bottom = parentRect.bottom
    }
}

override fun getGlobalVisibleRect(rect: Rect?, globalOffset: Point?): Boolean {
    val result = super.getGlobalVisibleRect(rect, globalOffset)
    rect?.let {
        getTextInputLayout()?.getGlobalVisibleRect(parentRect, globalOffset)
        rect.bottom = parentRect.bottom
    }
    return result
}

override fun requestRectangleOnScreen(rect: Rect?): Boolean {
    val result = super.requestRectangleOnScreen(rect)
    val parent = getTextInputLayout()
    // 10 is a random magic number to define a rectangle height.
    parentRect.set(0, parent?.height ?: 10 - 24, parent?.right ?: 0, parent?.height?: 0)
    parent?.requestRectangleOnScreen(parentRect, true /*immediate*/)
    return result
}

private fun getTextInputLayout(): TextInputLayout? {
    var parent = parent
    while (parent is View) {
        if (parent is TextInputLayout) {
            return parent
        }
        parent = parent.getParent()
    }
    return null
}



}
ajohnston777
  • 101
  • 3
  • 7
2

You should put everything in the ScrollView container so that user can at least scroll and see the error message. That is the only thing that worked for me.

<ScrollView
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="vertical" >
        ...
        other views
        ...
    </LinearLayout>
</ScrollView>
netpork
  • 552
  • 7
  • 20
  • 1
    While it does make accessing the error possible it does not show the error without the user knowing about it and looking for it. This is from a UX point of view a requirement and I'll update the question to reflect this. Thanks for your answer! – Franzaine Oct 28 '15 at 18:19
  • I agree but you should give the user, at least, a possibility to see the error message. – netpork Oct 28 '15 at 18:38
0

It's hacky but here's what I did to work around this:

Since in this case my TextInputLayout/EditText combo live within a RecyclerView, I simply scroll it up when I set the error:

textInputLayout.setError(context.getString(R.string.error_message))
recyclerView.scrollBy(0, context.convertDpToPixel(24f))

It works, but is definitely less than ideal. It'd be great if Google would fix this, as it's definitely a bug.

Chantell Osejo
  • 1,456
  • 15
  • 25
0

Super late to the party, but an easy solution to this is the following if you're already in a ScrollView/RecyclerView:

 editText.setOnFocusChangeListener { _, hasFocus ->
       if (hasFocus) {
         scrollBy(0,context.resources.getDimensionPixelSize(R.dimen.your_desired_dimension))
         // i would recommend 24dp
        }
  }
davy307
  • 309
  • 3
  • 16
-1

I just find out that if you put container in fixed height, keyboard leave space for error text

<FrameLayout 
    android:layout_width="match_parent"
    android:layout_height="75dp"
    android:layout_alignParentBottom="true">

  <android.support.design.widget.TextInputLayout
    android:id="@+id/text_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    app:errorEnabled="true"
    app:errorTextAppearance="@style/ErrorText">

     <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:imeOptions="actionGo"
        android:inputType="textPersonName"
        android:singleLine="true" />
  </android.support.design.widget.TextInputLayout>
</FrameLayout>
CrazyFox
  • 37
  • 1
  • 7
  • 1
    From the "android:layout_alignParentBottom" attribute in the FrameLayout I guess what you are doing is adding a "bottom bar" with the TextInputLauout. When window is resized by keyboard this makes the whole FrameLayout move up. This behaviour is NOT related with the question and the explained issue at all. – GuillermoMP Jun 13 '16 at 10:42