Android EditText帶清空按鈕&動畫


轉載請標明出處:
http://blog.csdn.net/tyzlmjj/article/details/49156765
本文出自:【M家傑的博客】

概述
安卓開發中有時經常會用到編輯框(登錄、找回密碼等場景),但是安卓的EditText竟然不自帶清空內容的功能,網上找了下資料自定義帶清空功能的EditText還是比較多的,但是自帶明文和密文切換的基本沒有,所以自己沒事幹做了一個帶點動畫效果的自定義EditText。

實現的功能:

    1. 清除按鈕
    2. 明文切換按鈕
    3. 按鈕進入的動畫
    4. 錯誤抖動效果
    5. material 風格

DEMO

先放上演示圖
1. 單獨EditText效果
單獨EditText效果
2. 配合TextInputLayout使用的效果
配合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分下載的。


參考資料


源碼下載

【CSDN】 【GitHub】


更新

刪除ClearEditText的init()方法中的setSingleLine();
原因:我原本想在代碼中調用這個方法就不用再XML中重複的寫android:singleLine="true"。但是我發現這很愚蠢!不是所有編輯框都需要單行的。而且在實際開發中這引起了一個BUG,當調用setSingleLine()方法時會重置編輯框的顯示模式爲SingleLineTransformationMethod,從而使原本在XML中設置的顯示方式失效(最明顯的就是設置密文顯示失效變明文),具體可查看TextView源碼中的applySingleLine方法。
GItHub代碼已更新,CSDN無法改文件所以暫不更新

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章