以前在为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(); }
activity需要实现TextWatcherCallBack,并且为CleanableEditText设置callback(如以下代码最后两句)
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。