之前的开发中,对于 EditText 的输入错误提醒,我常使用 Dialog 进行展示,如:

Dialog 提示

相比 Toast 而言,这是一种比较合适的交互,用户可以清楚地了解到为何自己的输入不合法,而不会因 Toast 显示时间太短导致错过具体信息。

但是,有一点不好的是增加了用户的点击成本。

当然也可以通过设定 Dialog 定时自动关闭来降低点击成本,不过由于 Dialog 在显示的时候会抢夺焦点,导致下方的 Activity 无法获取输入,所以这个成本并没有实际降低。

EditText 其实为我们提供了一种比较优雅的提示方式,它可以使用浮窗对非法输入进行提示,只需一行代码,比如用户输入为空,我们可以这样写:

if (TextUtils.isEmpty(editText.getText().toString())) {
    editText.setError("Content cannot be empty!");
}

在用户输入为空时,系统就会给出相应的提示:

浮窗提示

用户可以查阅提示信息的同时,又不会增加点击成本,很友好。

用法就这么简单,但如果文章这样结束就太草率了。

我们跟踪一下 setError() 方法的源码会发现,这个方法并不来源于 EditText,而是来源于其父类 TextView

@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
    ...
    /**
     * Sets the right-hand compound drawable of the TextView to the "error"
     * icon and sets an error message that will be displayed in a popup when
     * the TextView has focus.  The icon and error message will be reset to
     * null when any key events cause changes to the TextView's text.  If the
     * <code>error</code> is <code>null</code>, the error message and icon
     * will be cleared.
     */
    @android.view.RemotableViewMethod
    public void setError(CharSequence error) {
        if (error == null) {
            setError(null, null);
        } else {
            Drawable dr = getContext().getDrawable(com.android.internal.R.drawable.indicator_input_error);
            dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
            setError(error, dr);
        }
    }
}

如果你尝试直接对 TextView 调用 setError() 方法,会看到 TextView 虽然可以展示右侧的红色感叹号,但是并无法展示传入的内容:

不展示 Error Message

不过只需认真阅读 setError() 方法上的注释,你就会发现其中的端倪,仅当 TextView 拥有焦点时 Error Message 才会显示,这也是为什么 EditText 能够直接显示的原因,因为 EditText 本身就拥有焦点:

<resources>
    ...
    <style name="Widget.EditText">
        <item name="focusable">true</item>
        <item name="focusableInTouchMode">true</item>
        ...
    </style>
</resources>

也就是说,只要我们给 TextView 手动配置焦点,它也能够正常显示:

textView.setFocusableInTouchMode(true);
textView.requestFocus();
textView.setError("Error Message");

效果如下:

TextView 正常展示 Error Message

从注释中我们还能看到,当传入的错误信息为 null 时,错误信息和图标能够被清除。

噢对了,setError() 还提供了重载方法允许我们自行设置图标,实际上,上面的单个参数的 setError() 内部调用的也是这个重载方法:

@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
    ...
    /**
     * Sets the right-hand compound drawable of the TextView to the specified
     * icon and sets an error message that will be displayed in a popup when
     * the TextView has focus.  The icon and error message will be reset to
     * null when any key events cause changes to the TextView's text.  The
     * drawable must already have had {@link Drawable#setBounds} set on it.
     * If the <code>error</code> is <code>null</code>, the error message will
     * be cleared (and you should provide a <code>null</code> icon as well).
     */
    public void setError(CharSequence error, Drawable icon) {
        createEditorIfNeeded();
        mEditor.setError(error, icon);
        notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
    }
}

该重载方法需要传入一个 Drawable 对象,特别注意的是,该 Drawable 对象必须要调用 setBounds() 进行配置,本质上与我们之前在『Android 为 TextView 或 EditText 添加 ICON』介绍的差不多。

效果如下:

配置 Drawable 效果

更深入的源码就不在本文继续解析了,有兴趣可以自行查阅。