TextInputLayout中的错误文本被键盘遮挡

39

TextInputLayout包含一个EditText,该EditText接收用户的输入。使用Android Design Support Library引入的TextInputLayout时,我们应该将错误设置为持有EditText的TextInputLayout,而不是EditText本身。在编写UI时,只会专注于EditText而不是整个TextInputLayout,这可能导致键盘遮盖错误。请注意以下GIF中,用户必须先删除键盘才能看到错误消息。这与使用键盘设置IME操作一起会导致非常混乱的结果。

example error

布局xml代码:

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

将错误设置到TextInputLayout的Java代码:

uidTextInputLayout.setError("Incorrect cardnumber");

如何确保用户能够看到错误信息而无需进行操作?是否可以移动焦点?


1
我认为你应该在这里提交一个错误报告(https://code.google.com/p/android/issues/list)。 - natario
我同意你的观点,找到了一个漏洞(祝你好运)。 - natario
7个回答

9

更新:看起来这个问题可能已经在1.2.0-alpha03版本的库中得到解决。


为了确保错误消息在用户未采取任何操作时可见,我对TextInputLayout进行了子类化,并将其放置在一个ScrollView中。这让我可以在每次设置错误消息时滚动下来以显示错误消息。使用它的活动/片段类不需要进行任何更改。

输入图像描述

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://dev59.com/lVsX5IYBdhLWcg3wFsPV
        if (value == null) isErrorEnabled = false

        // work around https://dev59.com/6l0Z5IYBdhLWcg3wzC5W
        if (changed) scrollIfNeeded()
    }
}

以下是辅助方法:
/**
 * 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://dev59.com/uHA65IYBdhLWcg3wzyH8#36740277
        descendant.getDrawingRect(it)
        offsetDescendantRectToMyCoords(descendant, it)
    }.bottom
    return (bottom - height - scrollY).takeIf { it > 0 }
}

我还在同一个类中修复了TextInputLayout.setError()清除错误后留下空白的问题


嗨@arekolek,如果TextInputLayout在RecyclerView中,这个方法还有效吗?可能是的,因为RecyclerView包含ScrollView,但我想确认一下。 - davy307

4

这实际上是Google已知的问题。

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

他们提出的解决方案是创建一个自定义的TextInputEditText类。

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
    }
}```

当我使用这段代码时,我的自定义TextInputEditText无法获得焦点。还有其他人遇到这个问题吗?是否有解决方法? - ajohnston777
我不知道。也许是你的布局有问题。你尝试过请求焦点吗? - elFonzo
我用一个方法替换了getMyParent,这解决了我的问题。我会发布我的答案以进行演示。 - ajohnston777

3

@user2221404的回答对我无效,所以我将getMyParent()方法更改为以下内容:

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
}



}

2
你应该把所有内容放在 ScrollView容器中,这样用户至少可以滚动并查看错误消息。这是我唯一有效的解决方法。
<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>

1
虽然这样做可以让错误可访问,但如果用户不知道并寻找它,它不会显示错误。从用户体验的角度来看,这是一个要求,我将更新问题以反映这一点。感谢您的回答! - Franzaine
我同意,但你应该至少给用户一个查看错误信息的可能性。 - netpork

0

这个方法有点取巧,但是以下是我用来解决这个问题的方法:

由于在这种情况下我的TextInputLayout/EditText组合位于RecyclerView中,所以当我设置错误时,我只需将其向上滚动:

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

它能够工作,但肯定不是最理想的。如果谷歌能够修复这个问题,那就太好了,因为这肯定是一个漏洞。


0

虽然来晚了,但如果你已经在ScrollView/RecyclerView中,以下是一个简单的解决方案:

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

-1

我刚刚发现,如果您将容器设置为固定高度,则键盘会为错误文本留出空间。

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

1
从FrameLayout中的"android:layout_alignParentBottom"属性,我猜测你正在添加一个带有TextInputLauout的"底部栏"。当窗口被键盘调整大小时,这会使整个FrameLayout向上移动。这种行为与问题和解释的问题完全无关。 - GuillermoMP

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接