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