介绍#
很常见的一个功能,大部分app在登录界面都会实现这个功能了。因为在掘金上看了一篇类似的文章,所以决定自己实践一下。
下图为实现效果:
常见实现方法#
- 组合控件,EditText + Button
实现简单,可以单独使用。 - 自定义View,继承EditText,通过EditText自带的Drawable来实现。
布局复杂度低
继承EditText来实现一键清功能#
需要考虑的问题##
根据业务场景,有以下几个问题需要我们在实现中考虑到:
- 怎么添加清空按钮
- 怎么处理点击事件
- 处理清空按钮的显示状态
3.1 有文字时才显示清空按钮,没有文字则掩藏。
3.2 获取焦点时才显示清空按钮,没有焦点时则隐藏
3.3 EditText的setErrot方法调用后,清空按钮怎么处理 - 添加自定义的属性
实现流程##
带着上面的问题我们开始一步步实现自定义View。
步骤1:继承EditText,实现构造方法###
public class ClearableEditText extends EditText
implements EditText.OnFocusChangeListener {
public static final String TAG = "ClearableEditText";
public ClearableEditText(Context context) {
this(context, null);
}
public ClearableEditText(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.editTextStyle);
}
public ClearableEditText(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ClearableEditText(Context context, AttributeSet attrs, int defStyleAttr, int
defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr, defStyleRes);
}
}
步骤二:添加清空按钮###
TextView中有一个静态内部类Drawables, 这个类的作用是用来保存和显示TextView上下左右的Drawable。通过android:drawable*
来设置的drawable icon就保存在Drawables中。
通过调用 Drawable drawables[] = getCompoundDrawables();
方法可以获取到Drawables中的left, top, right, and bottom的Drawable数组。而drawables[2]正好就是显示在TextView右边的drawable icon,所以这个drawable 正好满足我们的需求。
private Drawable mClearDrawable;
/**
* Right Drawable 是否可见
*/
private boolean mIsClearVisible;
private void init(Context context, AttributeSet attrs, int defStyleAttr, int
defStyleRes) {
Drawable drawables[] = getCompoundDrawables();
mClearDrawable = drawables[2]; // Right Drawable;
// 第一次隐藏
setClearDrawableVisible(false);
}
步骤三:处理点击事件###
由于Drawable没办法接收处理TouchEvent,所以我们只能通过触摸区域来判断,当触摸事件的坐标在right drawable的范围内的时候就触发点击事件。
覆写onTouchEvent事件,这里我只判断了x轴的范围。那为什么不加上y轴的判断呢?个人认为没什么必要。
@Override
public boolean onTouchEvent(MotionEvent event) {
// error drawable 不显示 && clear drawable 显示 && action up
if (getError() == null && mIsClearVisible && event.getAction() == MotionEvent.ACTION_UP) {
float x = event.getX();
if (x >= getWidth() - getTotalPaddingRight() && x <= getWidth() - getPaddingRight()) {
Log.d(TAG, "点击清除按钮!");
clearText();
}
}
return super.onTouchEvent(event);
}
步骤四:处理清空按钮的显示状态###
有三种情况需要考虑:
1 有文字时才显示清空按钮,没有文字则掩藏。
2 获取焦点时才显示清空按钮,没有焦点时则隐藏
3 EditText的setErrot方法调用后,清空按钮怎么处理
为了解决1和2的两个问题,我们需要为EditText监听文字输入的状态和获取失去焦点的状态,因此我们需要实现onFocusChange和addTextChangedListener。
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (getError() == null) {
if (hasFocus) {
if (getText().length() > 0) {
setClearDrawableVisible(true);
}
} else {
setClearDrawableVisible(false);
}
}
}
// 添加TextChangedListener
addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Log.d(TAG, "onTextChanged " + s);
setClearDrawableVisible(s.length() > 0);
}
@Override
public void afterTextChanged(Editable s) {
}
});
setClearDrawableVisible的具体实现:
我们可以通过setCompoundDrawables来设置TextView的left、top、right、bottom drawable。传递null表示不需要显示。
/**
* 设置Right Drawable是否可见
*
* @param isVisible true for visible , false for invisible
*/
public void setClearDrawableVisible(boolean isVisible) {
setCompoundDrawables(getCompoundDrawables()[0], getCompoundDrawables()[1],
isVisible ? mClearDrawable : null, getCompoundDrawables()[3]);
mIsClearVisible = isVisible;
}
最后考虑第三种情况,EditText的setErrot方法可以给出校验提示,从下图中大致可以看出这个提示由两部分组成,一个icon和一个popup window,而这个icon正好占据了我们上面提到的right drawable的位置也就是我们的清除按钮需要用到的位置。那么error icon是不是也用了right drawable来实现的呢?
在TextView中有这样一个方法applyErrorDrawableIfNeeded,从方法名就可以猜到它就是用来设置error drawable的,最终通过mShowing[Drawables.RIGHT] = mDrawableError;
把error drawable 放到了Drawables.RIGHT中。哦,对了。。这一切都是基于LTR的Layout方向。
// then, if needed, assign the Error drawable to the correct location
if (mDrawableError != null) {
switch(layoutDirection) {
case LAYOUT_DIRECTION_RTL:
mDrawableSaved = DRAWABLE_LEFT;
mDrawableTemp = mShowing[Drawables.LEFT];
mDrawableSizeTemp = mDrawableSizeLeft;
mDrawableHeightTemp = mDrawableHeightLeft;
mShowing[Drawables.LEFT] = mDrawableError;
mDrawableSizeLeft = mDrawableSizeError;
mDrawableHeightLeft = mDrawableHeightError;
break;
case LAYOUT_DIRECTION_LTR:
default:
mDrawableSaved = DRAWABLE_RIGHT;
mDrawableTemp = mShowing[Drawables.RIGHT];
mDrawableSizeTemp = mDrawableSizeRight;
mDrawableHeightTemp = mDrawableHeightRight;
mShowing[Drawables.RIGHT] = mDrawableError;
mDrawableSizeRight = mDrawableSizeError;
mDrawableHeightRight = mDrawableHeightError;
break;
}
}
那么我们就可以处理第三种情况。覆写setError方法。想要知道error drawable是否显示,可以通过“getError() == null”来判断,为ture则表示不显示,false表示已显示。
@Override
public void setError(CharSequence error, Drawable icon) {
if (error != null) {
setClearDrawableVisible(true);
}
super.setError(error, icon);
}
为什么是覆写setError方法?我们可以通过TextView.sendAfterTextChanged来找到答案。当EditText中重新输入文字的后,sendAfterTextChanged会被调用,在sendAfterTextChanged方法中会调用hideErrorIfUnchanged,而 hideErrorIfUnchanged则是直接调用了setError(null, null)。通过setError(null, null)方法隐藏 error drawable。
步骤五:添加自定义属性###
attrs.xml中申明style
<declare-styleable name="ClearableEditText">
<attr name="right_drawable_color" format="color|reference" />
</declare-styleable>
在init函数中获取自定义属性并做相关处理。
final Resources.Theme theme = context.getTheme();
TypedArray a = theme.obtainStyledAttributes(attrs, R.styleable.ClearableEditText,
defStyleAttr, defStyleRes);
int rightDrawableColor = a.getColor(R.styleable.ClearableEditText_right_drawable_color,
Color.BLACK);
a.recycle();
// 给mRightDrawable上色
DrawableCompat.setTint(mClearDrawable, rightDrawableColor);