以前在为EditText添加左侧图标,以及右侧一个删除按钮时,经常是使用FrameLayout,当这样代码复用差,维护也麻烦。最好的方法是重写EditText实现该功能。现在看看效果图,后面再讲解实现方式。
重写之后的组件有如下功能,只有当EditText内容不为空,而且获得焦点,才会出现删除按钮,点击删除按钮则清空内容。代码如下:
public class CleanableEditText extends EditText {
//回调函数
private TextWatcherCallBack mCallback;
//右侧删除图标
private Drawable mDrawable;
private Context mContext;
public void setCallBack(TextWatcherCallBack mCallback) {
this.mCallback = mCallback;
}
public CleanableEditText(Context context) {
super(context);
this.mContext = context;
init();
}
public CleanableEditText(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
init();
}
public CleanableEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.mContext = context;
init();
}
public void init() {
mDrawable = mContext.getResources().getDrawable(R.drawable.ic_clear);
mCallback = null;
//重写了TextWatcher,在具体实现时就不用每个方法都实现,减少代码量
TextWatcher textWatcher = new TextWatcherAdapter() {
@Override
public void afterTextChanged(Editable s) {
//更新状态,检查是否显示删除按钮
updateCleanable(length(), true);
//如果有在activity中设置回调,则此处可以触发
if(mCallback != null)
mCallback.handleMoreTextChanged();
}
};
this.addTextChangedListener(textWatcher);
this.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
//更新状态,检查是否显示删除按钮
updateCleanable(length(), hasFocus);
}
});
}
//当内容不为空,而且获得焦点,才显示右侧删除按钮
public void updateCleanable(int length, boolean hasFocus){
if(length() > 0 && hasFocus)
setCompoundDrawablesWithIntrinsicBounds(null, null, mDrawable, null);
else
setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final int DRAWABLE_RIGHT = 2;
//可以获得上下左右四个drawable,右侧排第二。图标没有设置则为空。
Drawable rightIcon = getCompoundDrawables()[DRAWABLE_RIGHT];
if (rightIcon != null && event.getAction() == MotionEvent.ACTION_UP) {
//检查点击的位置是否是右侧的删除图标
//注意,使用getRwwX()是获取相对屏幕的位置,getX()可能获取相对父组件的位置
int leftEdgeOfRightDrawable = getRight() - getPaddingRight()
- rightIcon.getBounds().width();
if (event.getRawX() >= leftEdgeOfRightDrawable) {
setText("");
}
}
return super.onTouchEvent(event);
}
@Override
protected void finalize() throws Throwable {
mDrawable = null;
super.finalize();
}
}
实现的关键有两点,其中一点是得知道有API可以为EditText设置上下左右的图标,所以,就可以避免使用FrameLayout那种笨拙的方法(此处右侧图标在组件的代码中自动加入了,左侧图标则需要在XML代码中声明)。需要注意的另一点是得知道如何计算点击事件的位置,
实现过程遇到的一个小问题,在onTouchEvent()方法中,如果消耗事件(依据情况返回true或者false),则会出现一个问题,可以点击EditText,如果设置日志输出,可以发现action_down,action_move,action_up都输出了,代表点击事件正常,但是依然无法获得焦点。所以不难猜测EditText获得焦点可能和点击事件有关。如果强制调用requestFocus()方法,则可以“解决”这个问题,但是存在不稳定现象,有时会出bug,其中原因还没细究。于是,此处我不处理点击事件,直接返回super.onTouchEvent(event)
编码上用了几个小技巧
1,addTextChangedListener时发现经常需要实现三个方法,但是我们又只需重写一个,显得代码有点冗余,解决方式是增加一个adapter。
2,此处已经addTextChangedListener了,那么如果我在activity中也需要监听呢,如果直接监听,则会覆盖CleanableEditText中的监听。为了解决这个方法,我使用了一个回调接口,使用户在addTextChangedListener中有选择的做更多事情。实现方式如下
回调接口的代码如下:
public interface TextWatcherCallBack {
public void handleMoreTextChanged();
}
public class LoginActivity extends BaseActivity implements TextWatcherCallBack {
private ClearableAutoCompleteTextView accountView;
private CleanableEditText passwordText;
private Button login;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_login);
accountView = (ClearableAutoCompleteTextView) findViewById(R.id.et_account);
passwordText = (CleanableEditText) findViewById(R.id.et_password);
login = (Button) findViewById(R.id.bt_login);
accountView.setCallBack(this);
passwordText.setCallBack(this);
//,,,,,,,
}
}
该自定义组件的用法写到这,现在已用于我的一个XMPP即时通讯工具,托管在Github上。
传送门:Github地址
master分支是刚入门android写的代码,很差很渣,我自己都看不下去,不过唯一的价值是让我现在能参考smack开发包的API使用,当时也给我写android入门练手了。develop分支是痛下决心重新写的,今晚刚撸了登陆界面。接下来业余时间主要就维护这个项目,尽量多用上android各种知识。并写博客记录这些知识。欢迎fork欢迎提issue。