1. 程式人生 > >[Material Design] 教你做一個Material風格、動畫的按鈕(MaterialButton)

[Material Design] 教你做一個Material風格、動畫的按鈕(MaterialButton)

前段時間Android L 釋出了,相信看過釋出會瞭解過的朋友都為其中的 “Material Design” 感到由衷的驚豔吧!至少我是的。

在驚豔之餘感到由衷的遺憾,因為其必須在 ”Android L“ 上才能使用,MD,鬱悶啊。
之後便自己想弄一個點選動畫試試,此念頭一發不可收拾;乾脆一不做二不休,就重寫了一個 ”MaterialButton“ 控制元件出來。
在這裡不討論什麼是 :“Material Design” 。
在這裡將給大家分享一下我自己弄的 “Material Design” 風格的 ”MaterialButton“ 按鈕動畫實現。

預熱一下:


上面的兩張動畫相信大家都看過吧?是不是挺不錯的?反正我是覺得手機上有這樣的動畫是很爽的,比較手機是用來增加體驗的。但是這些動畫只能在Android L 才能體驗到,對於現在國內的 Android 廠商的情況來看,估計谷歌出新的版本的時候我們就能用上這個 L 版本了。

下面給大夥看看我做的 “MaterialButton” 按鈕:


效果還不錯吧?好了開始開工了。

介紹一下我的工具:“Android Studio” 當然大家用其他也行。

第一步:新建專案(這個任意,自己搗鼓吧)

第二步:新建自定義控制元件:在java資料夾上右擊選擇自定義控制元件:


取個名字:“MaterialButton


現在來看看多了一個類(MaterialButton),一個佈局檔案 “sample_material_button”,一個屬性檔案 “attrs_material_button


到這裡第二步完成了。多了3個檔案。

第三步:修改 “MaterialButton

” 類:

分為幾步走:刪除示例程式碼重新繼承自 “Button” 類複寫 “onTouchEvent()” 方法。完成後的程式碼:

public class MaterialButton extends Button {
    public MaterialButton(Context context) {
        super(context);
        init(null, 0);
    }

    public MaterialButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public MaterialButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        final TypedArray a = getContext().obtainStyledAttributes(
                attrs, R.styleable.MaterialButton, defStyle, 0);
        a.recycle();
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

}
是不是感覺乾淨多了?到此第三步完成了。

第四步:就是做實際的動畫了,在這裡需要給大家說說三個需要注意的東西:

1.點選事件響應,這個很好理解,在 “onTouchEvent()” 方法中完成,在該方法中我們需要完成的是點選後啟動一個動畫,同時需要獲取到當時點選的位置。

2.動畫,這裡的動畫不是放大動畫而是屬性動畫,說實話 這個要說清楚還真不是一點點就能說清楚的事情。簡單說就是在動畫中可以控制一個屬性的變化,而在這裡來說就是在 “MaterialButton” 類中建立一個寬度和一個顏色的屬性,然後在動畫中控制這兩個屬性的變化。

3.屬性的建立以及屬性的變化區域確定問題。

首先建立兩個屬性:

    private Paint backgroundPaint;
    private float radius;
    private Property<MaterialButton, Float> mRadiusProperty = new Property<MaterialButton, Float>(Float.class, "radius") {
        @Override
        public Float get(MaterialButton object) {
            return object.radius;
        }

        @Override
        public void set(MaterialButton object, Float value) {
            object.radius = value;
            //重新整理Canvas
            invalidate();
        }
    };

    private Property<MaterialButton, Integer> mBackgroundColorProperty = new Property<MaterialButton, Integer>(Integer.class, "bg_color") {
        @Override
        public Integer get(MaterialButton object) {
            return object.backgroundPaint.getColor();
        }

        @Override
        public void set(MaterialButton object, Integer value) {
            object.backgroundPaint.setColor(value);
        }
    };

兩個屬性對比一下可以發現在半徑的屬性 “set” 操作中呼叫了 “invalidate()” 方法,該方法的作用是告訴系統重新整理當前控制元件的 “Canvas”,也就是觸發一次:“onDraw(Canvas canvas)” 方法。

然後複寫 “onTouchEvent()” 方法如下:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //記錄座標
            paintX = event.getX();
            paintY = event.getY();
            //啟動動畫
            startAnimator();
        }
        return super.onTouchEvent(event);
    }
在該方法中,首先確定是否是點選下去的事件,然後記錄座標,並啟動動畫。

在啟動動畫方法 “startAnimator()” 方法中,我們這樣寫:

    private void startAnimator() {
        
        //計算半徑變化區域
        int start, end;

        if (getHeight() < getWidth()) {
            start = getHeight();
            end = getWidth();
        } else {
            start = getWidth();
            end = getHeight();
        }

        float startRadius = (start / 2 > paintY ? start - paintY : paintY) * 1.15f;
        float endRadius = (end / 2 > paintX ? end - paintX : paintX) * 0.85f;

        //新建動畫
        AnimatorSet set = new AnimatorSet();
        //新增變化屬性
        set.playTogether(
                //半徑變化
                ObjectAnimator.ofFloat(this, mRadiusProperty, startRadius, endRadius),
                //顏色變化 黑色到透明
                ObjectAnimator.ofObject(this, mBackgroundColorProperty, new ArgbEvaluator(), Color.BLACK, Color.TRANSPARENT)
        );
        // 設定時間
        set.setDuration((long) (1200 / end * endRadius));
        //先快後慢
        set.setInterpolator(new DecelerateInterpolator());
        set.start();
    }

在這一步我們需要知道有些按鈕並不是橫向的,所以長不一定大於寬度,所以需要先判斷獲取到最長與最短,然後進行計算獲取到開始的半徑與結束的半徑,這裡有一個我的思路圖:


我們知道在 Android 中都是以左上腳為圓心,然後右邊為X正數,下邊為Y正數。所以建立了如上座標系。

藍色矩形區域代表按鈕,藍色點代表點選的點。灰色矩形代表點選後的開始區域,然後4邊開始擴散開;以上就是一個簡單的原理。當然思路有些跳躍,如果不懂可以在下邊評論我都會進行回覆的。

第五步:畫畫,對就是畫畫;這一步就是利用上面的半徑和畫筆顏色進行實際的繪製。

這裡需要了解的是:

1:畫畫是在:“onDraw(Canvas canvas)” 方法中完成

2:在畫板(Canvas)上是分層級的,簡單說就是先畫背景然後畫房子,然後畫人,最後畫人的一些小細節 自底向上的流程

3:畫板每次畫 都是新的畫板,預示著你每次都需要從背景畫起然後才到人;在程式設計中就是每次 “onDraw(Canvas canvas)” 方法中的畫板(Canvas )都是新的(New)。

說了那麼多其實很簡單,因為複雜的都在上一步中完成了。 “onDraw(Canvas canvas)” 原始碼如下:

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        canvas.drawCircle(paintX, paintY, radius, backgroundPaint);
        canvas.restore();

        super.onDraw(canvas);
    }
在這裡我們先儲存了畫板的狀態,然後畫一個圓,然後恢復上一次的狀態,然後呼叫父類進行後面的繪製工作。

這裡解釋一下:

1.為什麼 “super.onDraw(canvas)” 需要放在最後呼叫?

因為畫板是分層級的,當呼叫 “super.onDraw(canvas)” 的時候進行的工作是繪製字型那些,如果放在前面呼叫那麼造成的後果是我們的圓會覆蓋到字型上面。所以我們需要先畫圓背景。

2.為什麼只有一次畫圓操作(canvas.drawCircle())?

因為在半徑屬性中呼叫了 “invalidate()” ,當每次變化半徑值的時候將進行一次 “onDraw(canvas)” 操作,也就畫一次圓,在一定時間內快速重複畫半徑逐漸增大的圓的時候就形成了動畫效果。

最後給出這次控制元件的程式碼:

public class MaterialButton extends Button {
    private Paint backgroundPaint;
    private float paintX, paintY, radius;

    public MaterialButton(Context context) {
        super(context);
        init(null, 0);
    }

    public MaterialButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public MaterialButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        final TypedArray a = getContext().obtainStyledAttributes(
                attrs, R.styleable.MaterialButton, defStyle, 0);
        a.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        canvas.drawCircle(paintX, paintY, radius, backgroundPaint);
        canvas.restore();

        super.onDraw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //記錄座標
            paintX = event.getX();
            paintY = event.getY();
            //啟動動畫
            startAnimator();
        }
        return super.onTouchEvent(event);
    }

    private void startAnimator() {

        //計算半徑變化區域
        int start, end;

        if (getHeight() < getWidth()) {
            start = getHeight();
            end = getWidth();
        } else {
            start = getWidth();
            end = getHeight();
        }

        float startRadius = (start / 2 > paintY ? start - paintY : paintY) * 1.15f;
        float endRadius = (end / 2 > paintX ? end - paintX : paintX) * 0.85f;

        //新建動畫
        AnimatorSet set = new AnimatorSet();
        //新增變化屬性
        set.playTogether(
                //半徑變化
                ObjectAnimator.ofFloat(this, mRadiusProperty, startRadius, endRadius),
                //顏色變化 黑色到透明
                ObjectAnimator.ofObject(this, mBackgroundColorProperty, new ArgbEvaluator(), Color.BLACK, Color.TRANSPARENT)
        );
        // 設定時間
        set.setDuration((long) (1200 / end * endRadius));
        //先快後慢
        set.setInterpolator(new DecelerateInterpolator());
        set.start();
    }


    private Property<MaterialButton, Float> mRadiusProperty = new Property<MaterialButton, Float>(Float.class, "radius") {
        @Override
        public Float get(MaterialButton object) {
            return object.radius;
        }

        @Override
        public void set(MaterialButton object, Float value) {
            object.radius = value;
            //重新整理Canvas
            invalidate();
        }
    };

    private Property<MaterialButton, Integer> mBackgroundColorProperty = new Property<MaterialButton, Integer>(Integer.class, "bg_color") {
        @Override
        public Integer get(MaterialButton object) {
            return object.backgroundPaint.getColor();
        }

        @Override
        public void set(MaterialButton object, Integer value) {
            object.backgroundPaint.setColor(value);
        }
    };
}

當然後續的工作還有:不同的顏色的按鈕按鈕屬性的問題

介於大家可能沒有 Android Studio 無法看到效果,特意把 Apk 上傳了,如果Eclipse不知道怎麼匯入的話 就加我QQ,我給你說一下!

地址:APK

這些我都在個人的專案中完成了,大家拿去試試: