1. 程式人生 > >Android自定義EditText——帶一鍵清除和密碼明文切換按鈕,支援多樣式選擇

Android自定義EditText——帶一鍵清除和密碼明文切換按鈕,支援多樣式選擇

      Android自定義View開始入坑,打算寫一些自定義控制元件練練手。

      這是一個自定義EditText,帶一鍵清除和密碼明文切換按鈕(可以傳入自定義圖片資源),可以自定義邊框顏色,還支援四種邊框樣式的選擇。

      原始碼已上傳 GitHub: 點選開啟連結    歡迎fork,start和批評指正哈。

      效果圖鎮樓。


      下面擼袖子開講了。從構造方法開始把,一般情況下構造方法三連擊就夠用了。因為這裡有自定義的 attrs 值,所以這裡用到了二個引數的構造方法。

      首先初始化畫筆,抗鋸齒就不說了,說下Paint.FILTER_BITMAP_FLAG 這個屬性,它表示用雙線性過濾來繪製Bitmap

有啥用呢? 影象在放大繪製的時候,預設使用的是最近鄰插值過濾,這種演算法簡單,但會出現馬賽克現象;而如果開啟了雙線性過濾,就可以讓影象繪製出來時顯得更加平滑。

      接下來是自定義屬性的初始化,如果在佈局xml中有指定自定義屬性的值,那麼在這裡會被讀取。如果沒有指定,那麼會使用預設的值。

    public PowerfulEditText(Context context) {
        this(context, null);
    }

    public PowerfulEditText(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public PowerfulEditText(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        //抗鋸齒和點陣圖濾波
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);

        //讀取xml檔案中的配置
        if (attrs != null) {
            TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PowerfulEditText);
            for (int i = 0; i < array.getIndexCount(); i++) {
                int attr = array.getIndex(i);

                switch (attr) {
                    case R.styleable.PowerfulEditText_clearDrawable:
                        mClearResId = array.getResourceId(attr, DEFAULT_CLEAR_RES);
                        break;

                    case R.styleable.PowerfulEditText_visibleDrawable:
                        mVisibleResId = array.getResourceId(attr, DEFAULT_VISIBLE_RES);
                        break;

                    case R.styleable.PowerfulEditText_invisibleDrawable:
                        mInvisibleResId = array.getResourceId(attr, DEFAULT_INVISIBLE_RES);
                        break;

                    case R.styleable.PowerfulEditText_BtnWidth:
                        mBtnWidth = array.getDimensionPixelSize(attr, DEFAULT_BUTTON_WIDTH);
                        break;

                    case R.styleable.PowerfulEditText_BtnSpacing:
                        mBtnPadding = array.getDimensionPixelSize(attr, DEFAULT_BUTTON_PADDING);
                        break;

                    case R.styleable.PowerfulEditText_borderStyle:
                        mBorderStyle = array.getString(attr);
                        break;

                    case R.styleable.PowerfulEditText_styleColor:
                        mStyleColor = array.getColor(attr, DEFAULT_STYLE_COLOR);
                        break;
                }
            }
            array.recycle();
        }

        //初始化按鈕顯示的Bitmap
        mBitmapClear = createBitmap(context, mClearResId, DEFAULT_CLEAR_RES);
        mBitmapVisible = createBitmap(context, mVisibleResId, DEFAULT_VISIBLE_RES);
        mBitmapInvisible = createBitmap(context, mInvisibleResId, DEFAULT_INVISIBLE_RES);
        //如果自定義,則使用自定義的值,否則使用預設值
        if (mBtnPadding == 0) {
            mBtnPadding = DEFAULT_BUTTON_PADDING;
        }
        if (mBtnWidth == 0) {
            mBtnWidth = DEFAULT_BUTTON_WIDTH;
        }
        //給文字設定一個padding,避免文字和按鈕重疊了
        mTextPaddingRight = mBtnPadding * 4 + mBtnWidth * 2;

        //按鈕出現和消失的動畫
        mGoneAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(ANIMATOR_TIME);
        mVisibleAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATOR_TIME);

        //是否是密碼樣式
        isPassword =
                getInputType() == (InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_CLASS_TEXT);

    }

      下面是View繪製的三大流程,measure,layout,draw。這裡對layout沒有做特殊主要處理,主要是mesaure和draw。

      首先是 measure。因為在控制元件裡繪製了按鈕,為了避免和text重疊,所以在measure時需要給顯示的text設定padding。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //設定右內邊距, 防止清除按鈕和文字重疊
        setPadding(getPaddingLeft(), getPaddingTop(), mTextPaddingRight, getPaddingBottom());
    }
       

      draw流程中主要的任務是繪製邊框和按鈕。

      邊框的多樣式選擇也是在這裡實現的。通過畫布Canvas的 drawxxx() 方法,就可以繪製出這些效果。這裡用到了三個。畫線:drawLine(...);  畫矩形:drawRect(...);  畫圓角矩形: drawRoundRect(...)。其中畫圓角矩形時,可能會遇到一些小麻煩,就是圓角上的線比四條邊上的線粗。所以有一些特殊處理,具體見使用canvas.drawRoundRect()時,解決四個圓角的線比較粗的問題

      這裡還給按鈕實現了消失和出現時的縮放動畫效果。控制元件獲取焦點且text不為空時,會顯示放大出現的動畫;失去焦點時,會顯示縮小後消失的動畫。

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mPaint.setStyle(Paint.Style.STROKE);

        //使用自定義顏色。如未定義,則使用預設顏色
        if (mStyleColor != -1) {
            mPaint.setColor(mStyleColor);
        } else {
            mPaint.setColor(DEFAULT_STYLE_COLOR);
        }

        //控制元件獲取焦點時,加粗邊框
        if (isFocused()) {
            mPaint.setStrokeWidth(DEFAULT_FOCUSED_STROKE_WIDTH);
        } else {
            mPaint.setStrokeWidth(DEFAULT_UNFOCUSED_STROKE_WIDTH);
        }

        //繪製清空和明文顯示按鈕
        drawBorder(canvas);

        //繪製邊框
        drawButtons(canvas);
    }

    private void drawBorder(Canvas canvas) {
        int width = getWidth();
        int height = getHeight();

        switch (mBorderStyle) {
            //矩形樣式
            case STYLE_RECT:
                setBackground(null);
                canvas.drawRect(0, 0, width, height, mPaint);
                break;

            //圓角矩形樣式
            case STYLE_ROUND_RECT:
                setBackground(null);
                float roundRectLineWidth = 0;
                if (isFocused()) {
                    roundRectLineWidth = DEFAULT_FOCUSED_STROKE_WIDTH / 2;
                } else {
                    roundRectLineWidth = DEFAULT_UNFOCUSED_STROKE_WIDTH / 2;
                }
                mPaint.setStrokeWidth(roundRectLineWidth);
                if (Build.VERSION.SDK_INT >= 21) {
                    canvas.drawRoundRect(
                            roundRectLineWidth/2, roundRectLineWidth/2, width - roundRectLineWidth/2, height - roundRectLineWidth/2,
                            DEFAULT_ROUND_RADIUS, DEFAULT_ROUND_RADIUS,
                            mPaint);
                } else {
                    canvas.drawRoundRect(
                            new RectF(roundRectLineWidth/2, roundRectLineWidth/2, width - roundRectLineWidth/2, height - roundRectLineWidth/2),
                            DEFAULT_ROUND_RADIUS, DEFAULT_ROUND_RADIUS,
                            mPaint);
                }
                break;

            //半矩形樣式
            case STYLE_HALF_RECT:
                setBackground(null);
                canvas.drawLine(0, height, width, height, mPaint);
                canvas.drawLine(0, height / 2, 0, height, mPaint);
                canvas.drawLine(width, height / 2, width, height, mPaint);
                break;

            //動畫特效樣式
            case STYLE_ANIMATOR:
                setBackground(null);
                if (isAnimatorRunning) {
                    canvas.drawLine(width / 2 - mAnimatorProgress, height, width / 2 + mAnimatorProgress, height, mPaint);
                    if (mAnimatorProgress == width / 2) {
                        isAnimatorRunning = false;
                    }
                } else {
                    canvas.drawLine(0, height, width, height, mPaint);
                }
                break;
        }
    }

    private void drawButtons(Canvas canvas) {
        if (isBtnVisible) {
            //播放按鈕出現的動畫
            if (mVisibleAnimator.isRunning()) {
                float scale = (float) mVisibleAnimator.getAnimatedValue();
                drawClearButton(scale, canvas);
                if (isPassword) {
                    drawVisibleButton(scale, canvas, isPasswordVisible);
                }
                invalidate();
            //繪製靜態的按鈕
            } else {
                drawClearButton(1, canvas);
                if (isPassword) {
                    drawVisibleButton(1, canvas, isPasswordVisible);
                }
            }
        } else {
            //播放按鈕消失的動畫
            if (mGoneAnimator.isRunning()) {
                float scale = (float) mGoneAnimator.getAnimatedValue();
                drawClearButton(scale, canvas);
                if (isPassword) {
                    drawVisibleButton(scale, canvas, isPasswordVisible);
                }
                invalidate();
            }
        }
    }

      這裡說的控制元件中的按鈕,其實都只是顯示一個圖片而已,並不能直接設定事件監聽。那麼如何實現點選效果,是通過判斷在控制元件中點選的位置來實現的。如果點選了控制元件中按鈕圖片顯示的區域,說明該按鈕應該響應事件。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_UP) {

            boolean clearTouched =
                    ( getWidth() - mBtnPadding - mBtnWidth < event.getX() )
                            && (event.getX() < getWidth() - mBtnPadding)
                            && isFocused();
            boolean visibleTouched =
                    (getWidth() - mBtnPadding * 3 - mBtnWidth * 2 < event.getX())
                            && (event.getX() < getWidth() - mBtnPadding * 3 - mBtnWidth)
                            && isPassword && isFocused();

            if (clearTouched) {
                setError(null);
                setText("");
                return true;
            } else if (visibleTouched) {
                if (isPasswordVisible) {
                    isPasswordVisible = false;
                    setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_CLASS_TEXT);
                    setSelection(getText().length());
                    invalidate();
                } else {
                    isPasswordVisible = true;
                    setInputType(InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
                    setSelection(getText().length());
                    invalidate();
                }
                return true;
            }
        }
        return super.onTouchEvent(event);
    }

        一些控制元件焦點變化時的處理是在回撥方法 onFocusChanged 中完成的。
    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        super.onFocusChanged(focused, direction, previouslyFocusedRect);

        //播放按鈕出現和消失動畫
        if (focused && getText().length() > 0) {
            if (!isBtnVisible) {
                isBtnVisible = true;
                startVisibleAnimator();
            }
        } else {
            if (isBtnVisible) {
                isBtnVisible = false;
                startGoneAnimator();
            }
        }

        //實現動畫特效樣式
        if (focused && mBorderStyle.equals(STYLE_ANIMATOR)) {
            isAnimatorRunning = true;
            mAnimator = ObjectAnimator.ofInt(this, BORDER_PROGRESS, 0, getWidth() / 2);
            mAnimator.setDuration(ANIMATOR_TIME);
            mAnimator.start();
        }
    }

     接下來,說下 動畫特效的樣式實現,就是控制元件獲取焦點時,邊框會有一個從中間向外面擴充套件的動畫。這裡用到了自定義屬性動畫。該動畫在上面 onFocusChanged 中啟動,然後通過變數mAnimatorProgress 來記錄當前動畫的進度。
    private boolean isAnimatorRunning = false;
    private int mAnimatorProgress = 0;

    //自定義屬性動畫
    private static final Property<PowerfulEditText, Integer> BORDER_PROGRESS
            = new Property<PowerfulEditText, Integer>(Integer.class, "borderProgress") {
        @Override
        public Integer get(PowerfulEditText powerfulEditText) {
            return powerfulEditText.getBorderProgress();
        }

        @Override
        public void set(PowerfulEditText powerfulEditText, Integer value) {
            powerfulEditText.setBorderProgress(value);
        }
    };


    protected void setBorderProgress(int borderProgress) {
        mAnimatorProgress = borderProgress;
        postInvalidate();
    }

    protected int getBorderProgress() {
        return mAnimatorProgress;
    }

      當mAnimatorProgress等於控制元件寬度的一半,說明動畫結束。
            //動畫特效樣式
            case STYLE_ANIMATOR:
                setBackground(null);
                if (isAnimatorRunning) {
                    canvas.drawLine(width / 2 - mAnimatorProgress, height, width / 2 + mAnimatorProgress, height, mPaint);
                    if (mAnimatorProgress == width / 2) {
                        isAnimatorRunning = false;
                    }
                } else {
                    canvas.drawLine(0, height, width, height, mPaint);
                }
                break;

        最後,說下該控制元件的使用把。一共有7個自定義屬性。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PowerfulEditText">
        <!--圖片資源-->
        <attr name="clearDrawable" format="reference"/>
        <attr name="visibleDrawable" format="reference"/>
        <attr name="invisibleDrawable" format="reference"/>
        <!--按鈕寬度大小-->
        <attr name="BtnWidth" format="dimension"/>
        <!--按鈕間距大小-->
        <attr name="BtnSpacing" format="dimension"/>
        <!--邊框顏色和樣式-->
        <attr name="styleColor" format="color"/>
        <attr name="borderStyle" format="string" />
    </declare-styleable>
</resources>

        使用示例如下:

    <com.wang.powerfuledittext.PowerfulEditText
        android:id="@+id/testThree"
        android:inputType="textPassword"
        app:clearDrawable="@drawable/clear_all"
        app:visibleDrawable="@drawable/visible"
        app:invisibleDrawable="@drawable/invisible"
        app:BtnWidth="@dimen/btn_edittext_width"
        app:BtnSpacing="@dimen/btn_edittext_padding"
        app:borderStyle="halfRect"
        app:styleColor="#ff0000"
        android:hint="半矩形樣式"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

        其中,邊框樣式的對應規則如下。

        1、矩形樣式:            app:borderStyle="rectangle"

        2、半矩形樣式:        app:borderStyle="halfRect"

        3、圓角矩形樣式:     app:borderStyle="roundRect"

        4、動畫特效樣式:     app:borderStyle="animator"

      控制元件抖動的效果也是利用動畫實現的。呼叫view例項的以下方法即可實現抖動。

mPEditText.startShakeAnimation()

        其他就不多囉嗦了。完整原始碼請戳本人 GitHub 把。點選開啟連結