TextInputLayout 中的错误文本被键盘覆盖

Error text in TextInputLayout is covered by keyboard

TextInputLayout 包含一个 EditText,后者又接收来自用户的输入。通过 Android 设计支持库引入的 TextInputLayout,我们应该将错误设置为包含 EditText 而不是 EditText 本身的 TextInputLayout。编写 UI 时,将只关注 EditText 而不是整个 TextInputLayout,这会导致键盘覆盖错误。在下面的 GIF 中,请注意用户必须先移除键盘才能看到错误消息。这与设置 IME 操作以继续使用键盘会导致非常混乱的结果。

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

Java 代码将错误设置为 TextInputLayout:

uidTextInputLayout.setError("Incorrect cardnumber");

如何确保错误消息在用户不采取行动的情况下可见?可以移动焦点吗?

您应该将所有内容都放在 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>

我刚刚发现,如果您将容器放在固定高度,键盘将 space 留给错误文本

<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.2.0-alpha03 版本中修复。


为了确保错误消息在用户不可见的情况下可见,我将 class 编辑 TextInputLayout 并将其放在 ScrollView 中。这让我可以在需要时向下滚动以显示错误消息,每次都会设置错误消息。使用它的 activity/fragment class 无需更改。

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 
        if (value == null) isErrorEnabled = false

        // work around 
        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 
        descendant.getDrawingRect(it)
        offsetDescendantRectToMyCoords(descendant, it)
    }.bottom
    return (bottom - height - scrollY).takeIf { it > 0 }
}

我也在同一个 class 中修复了

这很老套,但这是我为解决此问题所做的工作:

因为在这种情况下,我的 TextInputLayout/EditText 组合位于 RecyclerView 中,我只是在设置错误时将其向上滚动:

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

它有效,但绝对不理想。如果 Google 能解决这个问题就太好了,因为这绝对是一个错误。

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

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

他们提出的解决方案是创建自定义 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
    }
}```

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



}

聚会太晚了,但如果您已经在 ScrollView/RecyclerView 中,下面是一个简单的解决方案:

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