1. 程式人生 > >自定義控制元件之 SubmitBotton (提交按鈕)

自定義控制元件之 SubmitBotton (提交按鈕)

在 Android 中我覺得除了實現很多功能性很強的需求之外,最吸引我的就是各種炫酷的自定義控制元件,但是自定義控制元件這個東西沒有辦法用一種固定的模式來講解,因為自定義控制元件都是根據需求來定製的。同時這也說明只要程式猿牛逼,就沒有實現不了的功能。

之前有看到一個效果:

Android自定義動畫酷炫的提交按鈕


剛開始看到這個我也是一頭霧水,後來接觸了 Paint 類、 Canvas 類和屬性動畫後,對這個動畫的實現也有了一些自己的思路。雖然上面的博文中博主也一步一步教了如何實現,但是我還是當時看不懂,在能看懂的時候還是決定自己來實現一下這個控制元件。

這個控制元件大致思路分為三步,先是圓角矩形,然後文字慢慢消失並收縮成圓,最後圓升高並且在園中出現對勾,有這個基本思路後就可以分步來實現這個控制元件了。

1 準備工作

這一步不是重點,不過這一步中獲取自定義屬性和寬高的設定算是自定義控制元件的一個知識點,是一個擴充套件,詳細的講解可以看看洋神的部落格: Android 自定義View (一)

在構造方法中設定畫筆的各個屬性:
        //獲取自定義屬性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SubmitButton, defStyleAttr, 0);
        int background = typedArray.getColor(R.styleable.SubmitButton_sb_background, Color.rgb(210, 105, 30));
        sbText = typedArray.getString(R.styleable.SubmitButton_sb_text);
        int sbTextColor = typedArray.getColor(R.styleable.SubmitButton_sb_textColor, Color.rgb(255, 255, 255));
        sbTextSize = typedArray.getDimensionPixelSize(R.styleable.SubmitButton_sb_textSize, (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP, 18, getResources().getDisplayMetrics()));
        mPaint = new Paint();
        mPaint.setColor(background);
        mPaint.setAntiAlias(true);

        textPaint = new Paint();
        textPaint.setColor(sbTextColor);
        textPaint.setTextSize(sbTextSize);
        mBound = new Rect();
        textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);

        okPaint = new Paint();
        okPaint.setColor(sbTextColor);
        okPaint.setStrokeWidth(10);
        okPaint.setStrokeCap(Paint.Cap.ROUND);
        okPaint.setStrokeJoin(Paint.Join.ROUND);
        okPaint.setStyle(Paint.Style.STROKE);

重寫 onMeasure() 方法獲得控制元件的寬高:
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        setPadding(getPaddingLeft() + 60, getPaddingTop(), getPaddingRight() + 60, getPaddingBottom());
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
            float textWidth = mBound.width();
            width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
            float textHeight = mBound.height();
            height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
        }
        rectWidth = width;
        rectHeight = height;
        setMeasuredDimension(width, height);
    }

2 圓角矩形

畫圓角矩形的話是很簡單,是 Paint 和 Canvas 的基本使用就可以輕鬆實現。我之前有一篇部落格也記錄了這兩個類的基本使用: Paint 和 Canvas 類常用方法說明

根據上面獲取的寬高在 onDraw() 方法中畫一個圓角矩形:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        RectF rectF = new RectF(0, 0, rectWidth, rectHeight);
        canvas.drawRoundRect(rectF, 45, 45, mPaint);
        canvas.drawText(sbText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, textPaint);
    }

效果如下:

3 收縮成圓

從圓角矩形變成圓,我們需要設計一個動畫,至於這個動畫應該怎麼做,我們首先來看一張圖:

這個圖應該很能說明問題了,我們可以把圓也看做是圓角矩形,只不過我們需要不斷修改它 x 的起止位置,需要計算的就是 x 的起止位置。 x 需要變化的值是   getMeasuredWidth() / 2 - 圓的半徑,圓的直徑就是 getMeasuredHeight(),所以圓的半徑是 getMeasuredHeight() / 2,那麼 x 需要變化的值就是 getMeasuredWidth() / 2 - getMeasuredHeight() / 2,即 (getMeasuredWidth() - getMeasuredHeight()) / 2。  定義一個值作為這個控制元件的屬性,用來時刻更新 x 的起止位置:
    public int animationValue = 0;

修改一下 onDraw() 方法:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
        canvas.drawRoundRect(rectF, 45, 45, mPaint);
        canvas.drawText(sbText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, textPaint);
    }

定義一個方法,在這個方法中定義一個動畫集,包含兩個動畫,一個動畫用來修改 animationValue 的值(從 0 變化到 (getMeasuredWidth() - getMeasuredHeight()) / 2 ),另一個動畫用來讓文字慢慢消失:
    public void submit() {
        ObjectAnimator animationValueAnimator = ObjectAnimator.ofInt(this, "animationValue", 0, (getMeasuredWidth() - getMeasuredHeight()) / 2);
        ObjectAnimator alphaAnimator = ObjectAnimator.ofInt(textPaint, "alpha", 255, 0);
        AnimatorSet mAnimatorSet = new AnimatorSet();
        mAnimatorSet.play(animationValueAnimator).with(alphaAnimator);
        mAnimatorSet.setDuration(1000);
        animationValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                invalidate();
            }
        });
        mAnimatorSet.start();
    }

我們在構造方法中為這個自定義控制元件新增一個 OnClickListener,讓它點選後開始動畫:
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                submit();
            }
        });

至此,效果如下:

4 升高並畫對勾

升高很簡單,只需要改變這個控制元件在 y 軸 的位置即可,關鍵在於後面的畫對勾,我沒有實現原文中的效果,而是直接畫出了個勾,要實現慢慢打勾的效果暫時還不會(囧)。畫對勾其實可以用畫路線來實現,這一步的關鍵也是如何計算對勾的各個點,還是看一張圖:
對勾的位置也可以自己決定,我這裡提供了我的思路,首先我們應該知道圓心的位置,這個很簡單,圓心在 x 軸的位置為 getMeasuredWidth() / 2,在 y 軸的位置getMeasuredHeight() / 2,然後其他關鍵點我們可以通過圓心來計算,黑色的兩根線為以圓心為原點的 x 軸和 y 軸,垂直的兩根紅線和 y 軸為圓的四等分線,水平的兩根紅線為圓的三等分線。那麼對勾的三個關鍵點也就一目瞭然了,第一個點 x 軸的位置在第一個四等分點,y 軸的位置與圓心相同,第二個點 x 軸的位置與圓心相同,y 軸的位置在第二個三等分點,第三個點 x 軸的位置在第三個四等分點,y 軸的位置在第一個三等分點。可能我描述的不太準確,但是看圖應該能看明白,用程式碼確定位置的話是這樣:
            Path path = new Path();
            path.moveTo(rectWidth / 2 - rectHeight / 2 / 2, rectHeight / 2);
            path.lineTo(rectWidth / 2, rectHeight / 3 * 2);
            path.lineTo(rectWidth / 2 + rectHeight / 2 / 2, rectHeight / 3);

為了升高這個控制元件,我們需要在原來的動畫集中新增一個動畫,即該控制元件在 y 軸位置的變化動畫,這個動畫之後再顯示對勾,因為我都是在一直重畫這個控制元件,所以在前兩步時畫的是圓角矩形,最後一步需要多畫對勾,所以我定義了一個 boolean 型別的變數來讓它決定是畫圓角矩形還是對勾。
    private boolean drawOK = false;

增加改變 y 軸位置的動畫後 submit() 方法變為:
    public void submit() {
        ObjectAnimator animationValueAnimator = ObjectAnimator.ofInt(this, "animationValue", 0, (getMeasuredWidth() - getMeasuredHeight()) / 2);
        ObjectAnimator yAnimator = ObjectAnimator.ofFloat(SubmitButton.this, "y", getY(), getY() - 200);
        ObjectAnimator alphaAnimator = ObjectAnimator.ofInt(textPaint, "alpha", 255, 0);
        AnimatorSet mAnimatorSet = new AnimatorSet();
        mAnimatorSet.play(animationValueAnimator).with(alphaAnimator).before(yAnimator);
        mAnimatorSet.setDuration(1000);
        animationValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                invalidate();
            }
        });
        yAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                drawOK = true;
                invalidate();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        mAnimatorSet.start();
    }

onDraw() 方法變為:
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (drawOK) {
            RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
            canvas.drawRoundRect(rectF, 90, 90, mPaint);
            Path path = new Path();
            path.moveTo(rectWidth / 2 - rectHeight / 2 / 2, rectHeight / 2);
            path.lineTo(rectWidth / 2, rectHeight / 3 * 2);
            path.lineTo(rectWidth / 2 + rectHeight / 2 / 2, rectHeight / 3);
            canvas.drawPath(path, okPaint);
        } else {
        RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
        canvas.drawRoundRect(rectF, 45, 45, mPaint);
        canvas.drawText(sbText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, textPaint);
        }
    }

至此,這個控制元件算是完成了,最終效果如下:

5 總結

從事實際開發一年多,其實感覺自己的技術成長得很慢,很多東西自己還是處於模仿的階段,很希望自己能夠創新出一種庫,自己能夠讓技術流行起來。離職一段時間也發現工作中開發和憑興趣開發真的是有區別的,有任務在身的時候那種緊張感和責任感更強烈,所以實際工作經驗真的很重要,希望自己接下來找工作順利,同時也希望在今後一直堅持寫部落格記錄自己在開發道路上的成長。

6 原始碼

附上完整原始碼:
public class SubmitButton extends android.support.v7.widget.AppCompatButton {

    private Paint mPaint;
    private Paint textPaint;
    private Paint okPaint;
    private int rectWidth;
    private int rectHeight;
    public int animationValue = 0;
    private String sbText;
    private Rect mBound;
    private int sbTextSize;
    private boolean drawOK = false;

    public void setAnimationValue(int animationValue) {
        this.animationValue = animationValue;
    }

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

    public SubmitButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SubmitButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //獲取自定義屬性
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SubmitButton, defStyleAttr, 0);
        int background = typedArray.getColor(R.styleable.SubmitButton_sb_background, Color.rgb(210, 105, 30));
        sbText = typedArray.getString(R.styleable.SubmitButton_sb_text);
        int sbTextColor = typedArray.getColor(R.styleable.SubmitButton_sb_textColor, Color.rgb(255, 255, 255));
        sbTextSize = typedArray.getDimensionPixelSize(R.styleable.SubmitButton_sb_textSize, (int) TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP, 18, getResources().getDisplayMetrics()));
        mPaint = new Paint();
        mPaint.setColor(background);
        mPaint.setAntiAlias(true);

        textPaint = new Paint();
        textPaint.setColor(sbTextColor);
        textPaint.setTextSize(sbTextSize);
        mBound = new Rect();
        textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);

        okPaint = new Paint();
        okPaint.setColor(sbTextColor);
        okPaint.setStrokeWidth(10);
        okPaint.setStrokeCap(Paint.Cap.ROUND);
        okPaint.setStrokeJoin(Paint.Join.ROUND);
        okPaint.setStyle(Paint.Style.STROKE);

            setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    submit();
                }
            });
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int width;
        int height;
        setPadding(getPaddingLeft() + 60, getPaddingTop(), getPaddingRight() + 60, getPaddingBottom());
        if (widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        } else {
            textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
            float textWidth = mBound.width();
            width = (int) (getPaddingLeft() + textWidth + getPaddingRight());
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        } else {
            textPaint.getTextBounds(sbText, 0, sbText.length(), mBound);
            float textHeight = mBound.height();
            height = (int) (getPaddingTop() + textHeight + getPaddingBottom());
        }
        rectWidth = width;
        rectHeight = height;
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (drawOK) {
            RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
            canvas.drawRoundRect(rectF, 90, 90, mPaint);
            Path path = new Path();
            path.moveTo(rectWidth / 2 - rectHeight / 2 / 2, rectHeight / 2);
            path.lineTo(rectWidth / 2, rectHeight / 3 * 2);
            path.lineTo(rectWidth / 2 + rectHeight / 2 / 2, rectHeight / 3);
            canvas.drawPath(path, okPaint);
        } else {
        RectF rectF = new RectF(animationValue, 0, rectWidth - animationValue, rectHeight);
        canvas.drawRoundRect(rectF, 45, 45, mPaint);
        canvas.drawText(sbText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, textPaint);
        }
    }

    public void submit() {
        ObjectAnimator animationValueAnimator = ObjectAnimator.ofInt(this, "animationValue", 0, (getMeasuredWidth() - getMeasuredHeight()) / 2);
        ObjectAnimator yAnimator = ObjectAnimator.ofFloat(SubmitButton.this, "y", getY(), getY() - 200);
        ObjectAnimator alphaAnimator = ObjectAnimator.ofInt(textPaint, "alpha", 255, 0);
        AnimatorSet mAnimatorSet = new AnimatorSet();
        mAnimatorSet.play(animationValueAnimator).with(alphaAnimator).before(yAnimator);
        mAnimatorSet.setDuration(1000);
        animationValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                invalidate();
            }
        });
        yAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                drawOK = true;
                invalidate();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        mAnimatorSet.start();
    }
}