1. 程式人生 > >Android 自定義View實現動態炫酷按鈕

Android 自定義View實現動態炫酷按鈕

普通按鈕也就那麼幾種樣式,看著都審美疲勞,先放效果圖,演示Demo+原始碼在最後面


你會不會以為這個按鈕是集結了很多動畫的產物,我告訴你,並沒有。所有的實現都是基於自定義View,採用最底層的onDraw一點一點的畫出來的。沒有采用一丁點的動畫。雖然演示時間很短,但是要完成這麼多變化,還是挺吃力。

首先講解用法:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final AnimationButton button = (AnimationButton) findViewById(R.id.button);
//        button.setTextSizeTouch(25);  //設定按下時字型的大小,不設定有預設值
//        button.setStrokeProgress(10); //設定進度條的厚度,不設定有預設值
//        button.setColorBase(Color.GREEN); //設定整體基色,不設定有預設值
//        button.setColorBack(Color.GRAY); //設定進度條的背景色,不設定有預設值
//        button.setStroke(3); //設定邊框的厚度,不設定有預設值
//        button.setStrokeText(0); //設定文字的厚度,不設定有預設值
//        button.setTextSize(30); //設定文字的字型大小,不設定有預設值
//        button.setRound(30); //設定圓角,不設定有預設值
        button.setText("登入"); //設定文字,不設定有預設值
        button.setMode(AnimationButton.Mode.Hand_Finish); //設定進度條模式,不設定有預設值Mode.Auto_Finish
        button.setOnAnimationButtonClickListener(new AnimationButton.OnAnimationButtonClickListener() {
            @Override
            public void onClick() {
                //stopProgress方法 僅僅在button.setMode(AnimationButton.Mode.Hand_Finish);之後才有效。
                button.stopProgress();
            }
        });
    }
}
其實如果只需要最普通的功能,根本什麼都不用做。因為幾乎所有的引數都已經設定了固定內設定。在上面註釋掉的函式用法也是使用者唯一能用的幾個函式,其他函式雖然標示為public,但是卻是因為元件之內的方法傳遞,而不是給外界使用者呼叫的。因此大家如果想自定義樣式,可以呼叫註釋裡的方法。

下面開始原始碼講解,首先分解功能,所有的變化可以分為三個狀態:

1、預設狀態,也就是最初的狀態。主要完成的事情為:接收使用者的點選,改變背景的樣式從空心變為實心,動態改變文字的大小,然後就是逐漸得縮小成一個圓。

2、進度條狀態。主要完成進度條的遞進,演示圖上只轉了一圈。其實可以通過設定一個引數,轉動多圈直到使用者手動停止,甚至無限轉動

3、結束狀態。主要完成由圓的狀態變回圓角矩形的狀態,並呈現中間的Logo

既然分割出了狀態,那麼就採用狀態機+代理模式來實現這個功能吧。首先是狀態的列舉。

/**
 * Created by ccwxf on 2016/2/29.
 * 用於區別狀態,有:預設狀態、進度條狀態、結束狀態
 */
public enum Status {
    Default,
    Progress,
    Finish
}

然後是狀態機的介面,也就是所有的狀態需要完成的共同的事情:
/**
 * Created by ccwxf on 2016/2/29.
 */
public interface ButtonStatus {
    /**
     * @return 對應的Status值
     */
    Status getStatus();

    /**
     * 這個狀態的事件處理代理
     * @param mEvent
     * @return
     */
    boolean onTouchEvent(MotionEvent mEvent);

    /**
     * 這個狀態的繪製代理
     * @param mCanvas
     * @param mPaint
     */
    void onDraw(Canvas mCanvas, Paint mPaint);
}

然後我們實現按鈕的程式碼,也就是自定義View:
/**
 * Created by ccwxf on 2016/2/29.
 */
public class AnimationButton extends View {

    private static int Color_Base = Color.rgb(24, 204, 149);
    private static int Color_Back = Color.rgb(153, 153, 153);
    private static int Stroke = 3;
    private static int Stroke_Text = 0;
    private static int Stroke_Progress = 10;
    private static int Text_Size = 30;
    private static int Text_Size_Touch = 25;
    private static int Round = 30;
    private static String Text = "提交";

    private Mode mode = Mode.Auto_Finish;
    private int maxWidth;
    private int maxHeight;
    private int colorBase = Color_Base;
    private int colorBack = Color_Back;
    private int stroke = Stroke;
    private int strokeText = Stroke_Text;
    private int strokeProgress = Stroke_Progress;
    private int textSize = Text_Size;
    private int textSizeTouch = Text_Size_Touch;
    private int round = Round;
    private String text = Text;
    //是否停止進度條,由外界設定
    private boolean isProgressStop = false;

    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private ButtonStatus status;
    private OnAnimationButtonClickListener listener;

    public Mode getMode() {
        return mode;
    }

    public void setMode(Mode mode) {
        this.mode = mode;
    }

    public int getMaxWidth() {
        return maxWidth;
    }

    public int getMaxHeight() {
        return maxHeight;
    }

    public int getTextSizeTouch() {
        return textSizeTouch;
    }

    public void setTextSizeTouch(int textSizeTouch) {
        this.textSizeTouch = textSizeTouch;
    }

    public int getStrokeProgress() {
        return strokeProgress;
    }

    public void setStrokeProgress(int strokeProgress) {
        this.strokeProgress = strokeProgress;
    }

    public int getColorBase() {
        return colorBase;
    }

    public void setColorBase(int colorBase) {
        this.colorBase = colorBase;
    }

    public int getColorBack() {
        return colorBack;
    }

    public void setColorBack(int colorBack) {
        this.colorBack = colorBack;
    }

    public int getStroke() {
        return stroke;
    }

    public void setStroke(int stroke) {
        this.stroke = stroke;
    }

    public int getStrokeText() {
        return strokeText;
    }

    public void setStrokeText(int strokeText) {
        this.strokeText = strokeText;
    }

    public int getTextSize() {
        return textSize;
    }

    public void setTextSize(int textSize) {
        this.textSize = textSize;
    }

    public int getRound() {
        return round;
    }

    public void setRound(int round) {
        this.round = round;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public AnimationButton(Context context) {
        super(context);
    }

    public AnimationButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public AnimationButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (status != null) {
            return status.onTouchEvent(event);
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (status != null) {
            status.onDraw(canvas, mPaint);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        maxWidth = MeasureSpec.getSize(widthMeasureSpec);
        maxHeight = MeasureSpec.getSize(heightMeasureSpec);
        if (maxWidth != 0 && maxHeight != 0) {
            status = new DefaultStatus(this, maxWidth, maxHeight);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    /**
     * 改變整體狀態
     *
     * @param s      改變的狀態
     * @param width  目前的寬度
     * @param height 目前的高度
     */
    public void changeStatus(Status s, int width, int height, int centerX, int centerY) {
        switch (s) {
            case Default:
                break;
            case Progress:
                //改變狀態,進入進度條狀態
                status = new ProgressStatus(this, width, height, centerX, centerY);
                invalidate();
                break;
            case Finish:
                //進入結束狀態
                status = new FinishStatus(this, width, height, centerX, centerY);
                invalidate();
                break;
        }
    }

    /**
     * 外界設定停止進度條
     */
    public void stopProgress(){
        this.isProgressStop = true;
    }

    /**
     * 檢查是否進度條結束
     * @return
     */
    public boolean isProgressStop(){
        return isProgressStop;
    }

    public enum Mode{
        Auto_Finish,
        Hand_Finish
    }

    public interface OnAnimationButtonClickListener{
        void onClick();
    }

    public void setOnAnimationButtonClickListener(OnAnimationButtonClickListener listener){
        this.listener = listener;
    }

    public OnAnimationButtonClickListener getOnAnimationButtonClickListener(){
        return listener;
    }
}

上面實現了一堆的變數引數供使用者自定義。然後在onTouchEvent和onDraw方法中,將所有操作都代理出去。

然後我們來實現第一個狀態,也就是預設狀態:

/**
 * Created by ccwxf on 2016/2/29.
 */
public class DefaultStatus implements ButtonStatus {
    //分別表示處於預設狀態內部的四個子狀態
    private static final int Status_Default = 0;
    private static final int Status_Touch = 1;
    private static final int Status_Up = 2;
    private static final int Status_Next = 3;
    //重新整理width時的漸變數以及時間間距
    private static final int Delay_Next = 500;
    private static final int Delay_Frush = 10;
    private static final int Pixel_Frush = 8;
    //按鈕物件
    private AnimationButton button;
    //按鈕物件的長寬與中點座標(長寬為繪製的長寬,而不是控制元件的長寬)
    private int width;
    private int height;
    private int centerX;
    private int centerY;
    //子狀態變數
    private int status = Status_Default;
    private Handler handler = new Handler();

    public DefaultStatus(AnimationButton button, int width, int height) {
        this.button = button;
        this.width = width;
        this.height = height;
        this.centerX = width / 2;
        this.centerY = height / 2;
    }

    @Override
    public Status getStatus() {
        return Status.Default;
    }

    @Override
    public boolean onTouchEvent(MotionEvent mEvent) {
        switch (mEvent.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //按下時,切換到按下子狀態
                if(status == Status_Default){
                    status = Status_Touch;
                    button.invalidate();
                }
                return true;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                //擡起時,或者移除控制元件時,切換到擡起子狀態
                if(status == Status_Touch){
                    status = Status_Up;
                    button.invalidate();
                    //過500ms延遲後開始進行伸縮變化
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            //切換到next子狀態
                            if(status == Status_Up){
                                status = Status_Next;
                            }
                            if(status == Status_Next){
                                //若長寬不一致,則繼續漸變,否則改變狀態
                                if (width >= height) {
                                    width -= Pixel_Frush;
                                    button.invalidate();
                                    handler.postDelayed(this, Delay_Frush);
                                }else{
                                    button.changeStatus(Status.Progress, width, height, centerX, centerY);
                                }
                            }
                        }
                    }, Delay_Next);
                    //響應監聽器
                    AnimationButton.OnAnimationButtonClickListener listener = button.getOnAnimationButtonClickListener();
                    if(listener != null){
                        listener.onClick();
                    }
                }
                break;
        }
        return false;
    }

    @Override
    public void onDraw(Canvas mCanvas, Paint mPaint) {
        switch (status) {
            case Status_Default:
                onDrawDefault(mCanvas, mPaint);
                break;
            case Status_Touch:
                onDrawTouch(mCanvas, mPaint);
                break;
            case Status_Up:
                onDrawUp(mCanvas, mPaint);
                break;
            case Status_Next:
                onDrawNext(mCanvas, mPaint);
                break;
        }
    }

    /**
     * 繪製邊框,分為空心和實心兩種
     *
     * @param mCanvas 畫布
     * @param mPaint  畫筆
     * @param style   空心或者實心
     * @param padding 邊框補白
     */
    private void drawRound(Canvas mCanvas, Paint mPaint, Paint.Style style, int padding) {
        mPaint.setColor(button.getColorBase());
        int stroke = padding;
        if (style == Paint.Style.STROKE) {
            mPaint.setStyle(Paint.Style.STROKE);
            mPaint.setStrokeWidth(button.getStroke());
            stroke += button.getStroke() / 2;
        } else {
            mPaint.setStyle(Paint.Style.FILL);
        }
        //繪製邊框
        mCanvas.drawRoundRect(new RectF(stroke, stroke, width - stroke, height - stroke), button.getRound(), button.getRound(), mPaint);
    }

    /**
     * 畫文字,有字型大小和顏色的區別
     *
     * @param mCanvas   畫布
     * @param mPaint    畫筆
     * @param textSize  字型大小
     * @param textColor 字型顏色
     */
    private void drawText(Canvas mCanvas, Paint mPaint, int textSize, int textColor) {
        mPaint.setColor(textColor);
        mPaint.setStrokeWidth(button.getStrokeText());
        mPaint.setTextSize(textSize);
        Paint.FontMetrics metrics = mPaint.getFontMetrics();
        int textWidth = (int) mPaint.measureText(button.getText());
        int baseLine = (int) (height / 2 + (metrics.bottom - metrics.top) / 2 - metrics.bottom);
        mCanvas.drawText(button.getText(), (width - textWidth) / 2, baseLine, mPaint);
    }

    /**
     * 繪製預設狀態的按鈕
     *
     * @param mCanvas
     * @param mPaint
     */
    private void onDrawDefault(Canvas mCanvas, Paint mPaint) {
        drawRound(mCanvas, mPaint, Paint.Style.STROKE, 0);
        //繪製居中文字
        drawText(mCanvas, mPaint, button.getTextSize(), button.getColorBase());
    }

    /**
     * 繪製按下狀態的按鈕
     *
     * @param mCanvas
     * @param mPaint
     */
    private void onDrawTouch(Canvas mCanvas, Paint mPaint) {
        drawRound(mCanvas, mPaint, Paint.Style.FILL, button.getStroke());
        //繪製文字,字型要變化
        drawText(mCanvas, mPaint, button.getTextSizeTouch(), Color.WHITE);
    }

    /**
     * 繪製擡起狀態的按鈕
     *
     * @param mCanvas
     * @param mPaint
     */
    private void onDrawUp(Canvas mCanvas, Paint mPaint) {
        drawRound(mCanvas, mPaint, Paint.Style.FILL, 0);
        drawText(mCanvas, mPaint, button.getTextSize(), Color.WHITE);
    }

    /**
     * 繪製進入下一狀態的按鈕
     *
     * @param mCanvas
     * @param mPaint
     */
    private void onDrawNext(Canvas mCanvas, Paint mPaint) {
        mPaint.setColor(button.getColorBase());
        mPaint.setStyle(Paint.Style.FILL);
        //繪製邊框
        if (width >= height) {
            mCanvas.drawRoundRect(new RectF(centerX - width / 2, centerY - height / 2, centerX + width / 2, centerY + height / 2),
                    button.getRound(), button.getRound(), mPaint);
            //繪製文字
            mPaint.setColor(Color.WHITE);
            mPaint.setStrokeWidth(button.getStrokeText());
            mPaint.setTextSize(button.getTextSize());
            Paint.FontMetrics metrics = mPaint.getFontMetrics();
            int textWidth = (int) mPaint.measureText(button.getText());
            int baseLine = (int) (centerY + (metrics.bottom - metrics.top) / 2 - metrics.bottom);
            mCanvas.drawText(button.getText(), centerX - textWidth / 2, baseLine, mPaint);
        } else {
            mCanvas.drawOval(new RectF(centerX - width / 2, centerY - height / 2, centerX + width / 2, centerY + height / 2), mPaint);
        }
    }
}

然後是第二個狀態,進度條狀態:
/**
 * Created by ccwxf on 2016/2/29.
 */
public class ProgressStatus implements ButtonStatus {
    //轉圈的子狀態
    private static final int Status_Once = 0;
    private static final int Status_Twice = 1;
    //轉圈的漸變數
    private static final int Delay_Progress = 10;
    private static final int Angle_Progress = 5;
    private static final int Angle_Default = -90;
    private static final int Andle_Full = 270;

    private AnimationButton button;
    private int width;
    private int height;
    private int centerX;
    private int centerY;
    private int radius;
    private int status = Status_Once;
    //當前的進度
    private float progress = Angle_Default;
    private Handler handler = new Handler();

    public ProgressStatus(AnimationButton button, int width, int height, int centerX, int centerY) {
        this.button = button;
        this.width = width;
        this.height = height;
        this.centerX = centerX;
        this.centerY = centerY;
        //繪製起點是Stroke的中點,若不減去這個值,則onDraw時,會不完整。
        this.radius = (width - button.getStrokeProgress())  / 2;

        startProgress();
    }

    /**
     * 開始遞迴轉動進度條
     */
    private void startProgress() {
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if(progress >= Andle_Full){
                    //如果是手動結束模式
                    if(button.getMode() == AnimationButton.Mode.Hand_Finish && button.isProgressStop()){
                        //改變狀態
                        button.changeStatus(Status.Finish, width, height, centerX, centerY);
                        return;
                    }else{
                        if(status == Status_Once){
                            status = Status_Twice;
                        }else if(status == Status_Twice){
                            //如果是自動結束模式,則在第二次進度結束時改變狀態
                            if(button.getMode() == AnimationButton.Mode.Auto_Finish){
                                //改變狀態
                                button.changeStatus(Status.Finish, width, height, centerX, centerY);
                                return;
                            }else{
                                status = Status_Once;
                            }
                        }
                        //重置進度
                        progress = Angle_Default;
                    }
                }
                progress += Angle_Progress;
                button.invalidate();
                handler.postDelayed(this, Delay_Progress);
            }
        }, Delay_Progress);
    }

    @Override
    public Status getStatus() {
        return Status.Progress;
    }

    @Override
    public boolean onTouchEvent(MotionEvent mEvent) {
        return false;
    }

    @Override
    public void onDraw(Canvas mCanvas, Paint mPaint) {
        if(status == Status_Once){
            //繪製灰色背景
            onDrawCircle(mCanvas, mPaint, button.getColorBack());
            //繪製綠色進度
            onDrawArc(mCanvas, mPaint, button.getColorBase(), Angle_Default, progress);
        }else if(status == Status_Twice){
            //繪製綠色背景
            onDrawCircle(mCanvas, mPaint, button.getColorBase());
            //繪製灰色進度
            onDrawArc(mCanvas, mPaint, button.getColorBack(), Angle_Default, progress);
        }
    }

    /**
     * 畫一整個圓作為背景
     * @param mCanvas 畫布
     * @param mPaint 畫筆
     * @param color 顏色
     */
    private void onDrawCircle(Canvas mCanvas, Paint mPaint, int color){
        mPaint.setColor(color);
        mPaint.setStrokeWidth(button.getStrokeProgress());
        mPaint.setStyle(Paint.Style.STROKE);
        mCanvas.drawCircle(centerX, centerY, radius, mPaint);
    }

    /**
     * 畫一端圓弧
     * @param mCanvas 畫布
     * @param mPaint 畫筆
     * @param color 顏色
     * @param start 開始角度
     * @param stop 結束角度
     */
    private void onDrawArc(Canvas mCanvas, Paint mPaint, int color, float start, float stop){
        mPaint.setColor(color);
        mPaint.setStrokeWidth(button.getStrokeProgress());
        mPaint.setStyle(Paint.Style.STROKE);
        //第三個引數是掃過的角度,起點0預設為右邊
        mCanvas.drawArc(new RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius),
                start, stop - start, false, mPaint);
    }
}
最後一個狀態:
/**
 * Created by ccwxf on 2016/2/29.
 */
public class FinishStatus implements ButtonStatus {

    private static final int Status_Stretch = 0;
    private static final int Status_Finish = 1;
    private static final int Stroke_Over = 10;
    private static final String Text_Over = "√";
    private static final int Text_Over_Size = 40;
    private static final int Delay_Stretch = 10;
    private static final int Pixel_Stretch = 8;

    private AnimationButton button;
    private int width;
    private int height;
    private int centerX;
    private int centerY;
    private int status = Status_Stretch;
    private Handler handler = new Handler();

    public FinishStatus(AnimationButton button, int width, int height, int centerX, int centerY) {
        this.button = button;
        this.width = width;
        this.height = height;
        this.centerX = centerX;
        this.centerY = centerY;

        startStretch();
    }

    /**
     * 開始伸展背景
     */
    private void startStretch() {
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                if(width < button.getMaxWidth()){
                    width += Pixel_Stretch;
                    button.invalidate();
                    handler.postDelayed(this, Delay_Stretch);
                }else{
                    width = button.getMaxWidth();
                    if(status == Status_Stretch){
                        status = Status_Finish;
                    }
                    button.invalidate();
                }
            }
        }, Delay_Stretch);
    }

    @Override
    public Status getStatus() {
        return Status.Finish;
    }

    @Override
    public boolean onTouchEvent(MotionEvent mEvent) {
        return false;
    }

    @Override
    public void onDraw(Canvas mCanvas, Paint mPaint) {
        //繪製背景
        mPaint.setColor(button.getColorBase());
        mPaint.setStyle(Paint.Style.FILL);
        mCanvas.drawRoundRect(new RectF(centerX - width / 2, centerY - height / 2, centerX + width / 2, centerY + height / 2 ),
                button.getRound(), button.getRound(), mPaint);
        //繪製圖片
        if(status == Status_Finish){
            mPaint.setColor(Color.WHITE);
            mPaint.setStrokeWidth(Stroke_Over);
            mPaint.setTextSize(Text_Over_Size);
            Paint.FontMetrics metrics = mPaint.getFontMetrics();
            int textWidth = (int) mPaint.measureText(Text_Over);
            int baseLine = (int) (height / 2 + (metrics.bottom - metrics.top) / 2 - metrics.bottom);
            mCanvas.drawText(Text_Over, (width - textWidth) / 2, baseLine, mPaint);
        }
    }
}

好了上面就是所有的原始碼了。雖然被我概括成三個大狀態,但是如果分細一點的話,大概需要9個狀態。在各個大狀態程式碼裡面的子狀態就是這個了。

怎麼使用呢,也非常簡單,因為大部分的引數都有內設值了。

/**
 * Created by ccwxf on 2016/2/29.
 */
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final AnimationButton button = (AnimationButton) findViewById(R.id.button);
//        button.setTextSizeTouch(25);  //設定按下時字型的大小,不設定有預設值
//        button.setStrokeProgress(10); //設定進度條的厚度,不設定有預設值
//        button.setColorBase(Color.GREEN); //設定整體基色,不設定有預設值
//        button.setColorBack(Color.GRAY); //設定進度條的背景色,不設定有預設值
//        button.setStroke(3); //設定邊框的厚度,不設定有預設值
//        button.setStrokeText(0); //設定文字的厚度,不設定有預設值
//        button.setTextSize(30); //設定文字的字型大小,不設定有預設值
//        button.setRound(30); //設定圓角,不設定有預設值
        button.setText("登入"); //設定文字,不設定有預設值
        button.setMode(AnimationButton.Mode.Hand_Finish); //設定進度條模式,不設定有預設值Mode.Auto_Finish
        button.setOnAnimationButtonClickListener(new AnimationButton.OnAnimationButtonClickListener() {
            @Override
            public void onClick() {
                //stopProgress方法 僅僅在button.setMode(AnimationButton.Mode.Hand_Finish);之後才有效。
                button.stopProgress();
            }
        });
    }
}

下面給出Demo,原始碼在Demo裡的一個package裡面。求人氣啊,每次人氣都那麼低