轉載請標明出處:
http://blog.csdn.net/tyzlmjj/article/details/49156765
本文出自:【M家傑的博客】
概述
安卓開發中有時經常會用到編輯框(登錄、找回密碼等場景),但是安卓的EditText竟然不自帶清空內容的功能,網上找了下資料自定義帶清空功能的EditText還是比較多的,但是自帶明文和密文切換的基本沒有,所以自己沒事幹做了一個帶點動畫效果的自定義EditText。
實現的功能:
1. 清除按鈕
2. 明文切換按鈕
3. 按鈕進入的動畫
4. 錯誤抖動效果
5. material 風格
DEMO
先放上演示圖
1. 單獨EditText效果
2. 配合TextInputLayout使用的效果
實現方式
網上最常見的實現帶清空按鈕的EditText方式是:設置drawableRight爲刪除按鈕–>監聽觸控事件做清空操作
這種方式很簡單,但也存在侷限性,比如drawableRight被佔用、動畫效果不可實現等。因爲我自己比較喜歡帶點動效的UI,所以換了一種方案實現:
重寫onDraw()方法 –>利用屬性動畫刷新實現動效–>監聽觸控事件做清空操作
完整的代碼
先把完整的ClearEditText 的代碼放上來,方便閱讀和直接複製,後面詳細講解關鍵代碼
public class ClearEditText extends AppCompatEditText {
//按鈕資源
private final int CLEAR = R.drawable.clearfill;
//動畫時長
private final int ANIMATOR_TIME = 200;
//按鈕左右間隔,單位DP
private final int INTERVAL = 5;
//清除按鈕寬度,單位DP
private final int WIDTH_OF_CLEAR = 23;
//間隔記錄
private int Interval;
//清除按鈕寬度記錄
private int mWidth_clear;
//右內邊距
private int mPaddingRight;
//清除按鈕的bitmap
private Bitmap mBitmap_clear;
//清除按鈕出現動畫
private ValueAnimator mAnimator_visible;
//消失動畫
private ValueAnimator mAnimator_gone;
//是否顯示的記錄
private boolean isVisible = false;
//右邊添加其他按鈕時使用
private int mRight = 0;
public ClearEditText(final Context context) {
super(context);
init(context);
}
public ClearEditText(final Context context,final AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ClearEditText(final Context context,final AttributeSet attrs,final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
//setSingleLine();這個方法不推薦寫在代碼中,原因請看博客尾部更新
mBitmap_clear = createBitmap(CLEAR,context);
Interval = dp2px(INTERVAL);
mWidth_clear = dp2px(WIDTH_OF_CLEAR);
mPaddingRight = Interval + mWidth_clear + Interval ;
mAnimator_gone = ValueAnimator.ofFloat(1f, 0f).setDuration(ANIMATOR_TIME);
mAnimator_visible = ValueAnimator.ofInt(mWidth_clear + Interval,0).setDuration(ANIMATOR_TIME);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//設置內邊距
setPadding(getPaddingLeft(), getPaddingTop(), mPaddingRight+ mRight, getPaddingBottom());
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG));//抗鋸齒
if (mAnimator_visible.isRunning()) {
int x = (int) mAnimator_visible.getAnimatedValue();
drawClear(x,canvas);
invalidate();
}else if (isVisible){
drawClear(0,canvas);
}
if(mAnimator_gone.isRunning()){
float scale = (float) mAnimator_gone.getAnimatedValue();
drawClearGone(scale, canvas);
invalidate();
}
}
/**
* 繪製清除按鈕出現的圖案
* @param translationX 水平移動距離
* @param canvas
*/
protected void drawClear( int translationX,Canvas canvas){
int right = getWidth()+getScrollX() - Interval - mRight +translationX;
int left = right-mWidth_clear;
int top = (getHeight()-mWidth_clear)/2;
int bottom = top + mWidth_clear;
Rect rect = new Rect(left,top,right,bottom);
canvas.drawBitmap(mBitmap_clear, null, rect, null);
}
/**
* 繪製清除按鈕消失的圖案
* @param scale 縮放比例
* @param canvas
*/
protected void drawClearGone( float scale,Canvas canvas){
int right = (int) (getWidth()+getScrollX()- Interval - mRight -mWidth_clear*(1f-scale)/2f);
int left = (int) (getWidth()+getScrollX()- Interval - mRight -mWidth_clear*(scale+(1f-scale)/2f));
int top = (int) ((getHeight()-mWidth_clear*scale)/2);
int bottom = (int) (top + mWidth_clear*scale);
Rect rect = new Rect(left,top,right,bottom);
canvas.drawBitmap(mBitmap_clear, null, rect, null);
}
/**
* 開始清除按鈕的顯示動畫
*/
private void startVisibleAnimator() {
endAnaimator();
mAnimator_visible.start();
invalidate();
}
/**
* 開始清除按鈕的消失動畫
*/
private void startGoneAnimator() {
endAnaimator();
mAnimator_gone.start();
invalidate();
}
/**
* 結束所有動畫
*/
private void endAnaimator(){
mAnimator_gone.end();
mAnimator_visible.end();
}
/**
* Edittext內容變化的監聽
*/
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
if(text.length()>0) {
if (!isVisible) {
isVisible = true;
startVisibleAnimator();
}
}else{
if (isVisible) {
isVisible = false;
startGoneAnimator();
}
}
}
/**
* 觸控執行的監聽
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
boolean touchable = ( getWidth() - Interval - mRight - mWidth_clear < event.getX() ) && (event.getX() < getWidth() - Interval - mRight);
if (touchable) {
setError(null);
this.setText("");
}
}
return super.onTouchEvent(event);
}
/**
* 開始晃動動畫
*/
public void startShakeAnimation(){
if(getAnimation() == null){
this.setAnimation(shakeAnimation(4));
}
this.startAnimation(getAnimation());
}
/**
* 晃動動畫
* @param counts 0.5秒鐘晃動多少下
* @return
*/
private Animation shakeAnimation(int counts){
Animation translateAnimation = new TranslateAnimation(0, 10, 0, 0);
translateAnimation.setInterpolator(new CycleInterpolator(counts));
translateAnimation.setDuration(500);
return translateAnimation;
}
/**
* 給圖標染上當前提示文本的顏色並且轉出Bitmap
* @param resources
* @param context
* @return
*/
public Bitmap createBitmap(int resources,Context context) {
final Drawable drawable = ContextCompat.getDrawable(context, resources);
final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTint(wrappedDrawable, getCurrentHintTextColor());
return drawableToBitamp(wrappedDrawable);
}
/**
* drawable轉換成bitmap
* @param drawable
* @return
*/
private Bitmap drawableToBitamp(Drawable drawable)
{
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
Bitmap bitmap = Bitmap.createBitmap(w, h, config);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
drawable.draw(canvas);
return bitmap;
}
public int dp2px(float dipValue) {
final float scale = getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
繼承EditText
自定義View首先要做的一步一般是繼承,這裏有兩個可以選擇EditText和AppCompatEditText,推薦繼承後者,這樣可以在5.0以下的版本也應用material 風格。顏色可以在樣式中修改colorAccent的顏色改變如下:
<item name="colorAccent">@color/colorAccent</item>
如果不想使用material的樣式那麼繼承任意一個都區別不大,樣式都可以通過修改背景改變。
構造方法
自定義View,繼承類之後馬上要寫的是構造方法,大多數情況下會像下面那樣寫(在這裏不推薦這樣寫):
public ClearEditText(final Context context) {
this(context,null);
}
public ClearEditText(final Context context,final AttributeSet attrs) {
this(context, attrs,0);
}
public ClearEditText(final Context context,final AttributeSet attrs,final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
以上的寫法很常見,也很正常,但是我繼承 AppCompatEditText是爲了應用material的風格,AppCompatEditText源碼的構造函數中就應用這種樣式的代碼都寫好了,所以我們需要應用父類的構造函數。正確的寫法:
public ClearEditText(final Context context) {
super(context);
init(context);
}
public ClearEditText(final Context context,final AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ClearEditText(final Context context,final AttributeSet attrs,final int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
關鍵代碼解析
onMeasure
在onMeasure方法中主要是設置了右內邊距,爲了讓輸入框內容過長時按鈕和文本內容不重疊
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//設置內邊距
setPadding(getPaddingLeft(), getPaddingTop(), mPaddingRight+ mRight, getPaddingBottom());
}
onDraw
這次代碼中最關鍵的就是改寫了這個onDraw方法,在這裏主要是運用了ValueAnimator,讓它運行並且在這裏判斷是否是運行狀態。然後通過動畫的參數去計算位置,然後將按鈕畫出來,再調用invalidate()刷新(invalidate()是一個刷新視圖的方法,調用之後會自動的再去重繪視圖,即運行onDraw方法)。而我自己寫的drawClear()和drawClearGone()2個方法主要是計算動畫參數,然後用canvas將按鈕圖案畫出來。
protected void onDraw(Canvas canvas) {
....
if (mAnimator_visible.isRunning()) {
int x = (int) mAnimator_visible.getAnimatedValue();
drawClear(x,canvas);
invalidate();
}else if (isVisible){
drawClear(0,canvas);
}
if(mAnimator_gone.isRunning()){
float scale = (float) mAnimator_gone.getAnimatedValue();
drawClearGone(scale, canvas);
invalidate();
}
}
內容改變的監聽onTextChanged
網上很多例子中說需要實現TextWatcher接口來監聽EditText的內容變化,其實不需要,既然已經繼承了EditText可以直接重寫onTextChanged()方法。在這裏實現文本內容改變後的操作。這裏的操作就是判斷下刪除按鈕是否顯示。
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
if(text.length()>0) {
if (!isVisible) {
isVisible = true;
startVisibleAnimator();
}
}else{
if (isVisible) {
isVisible = false;
startGoneAnimator();
}
}
}
觸控事件監聽
覆蓋父類的onTouchEvent()方法實現監聽,然後在手指擡起的時候判斷擡起點的座標是否在清除按鈕的範圍內,如果是就清空文本,不需要在寫別的代碼了,因爲this.setText("")
方法會觸發上面說到的onTextChanged()方法。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
boolean touchable = ( getWidth() - Interval - mRight - mWidth_clear < event.getX() ) && (event.getX() < getWidth() - Interval - mRight);
if (touchable) {
setError(null);
this.setText("");
}
}
return super.onTouchEvent(event);
}
額外的晃動動畫
本來我自己是沒寫這個動畫的,不過看見網上很多的自定義EditText裏面都放了這段動畫,代碼還基本都一樣,可能都是CTRL C的吧!所以我也低調的複製進去了!用不到的可以把這段刪了。
在代碼中調用startShakeAnimation()
方法就可以實現晃動動畫了。
/**
* 開始晃動動畫
*/
public void startShakeAnimation(){
if(getAnimation() == null){
this.setAnimation(shakeAnimation(4));
}
this.startAnimation(getAnimation());
}
/**
* 晃動動畫
* @param counts 0.5秒鐘晃動多少下
* @return
*/
private Animation shakeAnimation(int counts){
Animation translateAnimation = new TranslateAnimation(0, 10, 0, 0);
translateAnimation.setInterpolator(new CycleInterpolator(counts));
translateAnimation.setDuration(500);
return translateAnimation;
}
錯誤提示
EditText是自帶錯誤提示的,可能很多同學不知道,這裏做個演示。
單獨使用EditText的時候,調用:EditText.setError("輸入的密碼錯誤");
效果
配合TextInputLayout使用,調用TextInputLayout.setError("輸入的密碼錯誤");
效果
關於PasswordEditText
PasswordEditText是我繼承ClearEditText然後再添加了一個按鈕實現的,實現的原理基本相同。這裏不貼出來的原因是因爲這個明文顯示的按鈕在絕大多數APP中都不常見,所以個人覺得沒必要佔用篇幅講這個,想看的同學可以下載下面的源碼查看,CSDN資源都是0分下載的。
參考資料
- Android圖形動畫 http://www.cnblogs.com/zhucai/p/android-graphics-animation.html
- 讓你的 EditText 全部清除 http://mrfu.me/android/2015/07/30/allclear_edittext/
源碼下載
更新
刪除ClearEditText的init()方法中的setSingleLine();
原因:我原本想在代碼中調用這個方法就不用再XML中重複的寫android:singleLine="true"
。但是我發現這很愚蠢!不是所有編輯框都需要單行的。而且在實際開發中這引起了一個BUG,當調用setSingleLine()
方法時會重置編輯框的顯示模式爲SingleLineTransformationMethod
,從而使原本在XML中設置的顯示方式失效(最明顯的就是設置密文顯示失效變明文),具體可查看TextView源碼中的applySingleLine
方法。
GItHub代碼已更新,CSDN無法改文件所以暫不更新