1. 程式人生 > >一個酷炫的button變化動畫開源庫原始碼分析—Android morph Button(一)

一個酷炫的button變化動畫開源庫原始碼分析—Android morph Button(一)

最近很是喜愛一些酷炫的動畫效果,特意在github上找了一些,看看他們是怎麼做到的,做個分析,順便可以對自定義控制元件和動畫有進一步的認識。
先來看下這個庫中button的變化效果是什麼樣的:

是的傳送到

這裡寫圖片描述

是不是很酷炫,而且中間的變化過程很舒服,沒有僵硬的感覺,應用的場景也比較廣:只要點選按鈕,執行一個操作之後,返回結果,這個結果以對錯表示,如果是一個耗時的操作還可以顯示執行的進度,有很好的使用者體驗。比如點選按鈕後,在後臺進行下載、使用者點選按鈕進行登入等。

先分析第一個動畫效果:
稍微複雜的動畫一般是用屬性動畫來做了,對多個屬性進行同時變化,仔細觀察這個動畫效果可以看到,有width的變化,由長方形變成了圓形,必然有CornerRadius的變化,變化的過程中背景顏色也有變化,最後顯示通過和沒通過的ICON,來看下ObjectAnimator使用的方法,target就是要變化的物件,propertyName就是要變化的屬性,現在要變化的屬性已經有了,就是上面說的:width、cornerRadius、color等,那麼target應該是什麼?

直接是button本身嗎?我們知道某個屬性變化(如color)是依據target中的setColor()方法來動態設定color的值,也就是button中藥提供setColor()、setCornerRadius()這樣的方法,來更新對應的值到介面上,一般最後還有呼叫invalidate()方法來重新整理介面展示變化的效果。但是這樣實現比較麻煩,這些方法都要我們自己提供。那麼Android Morphing Button這個庫是怎麼做的呢?

其實我們想象這個button動畫真的變化的其實就是它的background,這個庫就是將backgroud設定為一個GradientDrawable,然後對這個GradientDrawable進行變化,也就事target就是這個GradientDrawable,GradientDrawable本身就有setColor、setCornerRadius、setStroke這些方法,並且會自動重新整理UI,這樣就不不用我們自己去寫這些方法來重繪,大體的思路就是這樣的,接下來分析具體的程式碼。

  public static ObjectAnimator ofInt(Object target, String propertyName, int... values)

1. 具體使用:

// sample demonstrate how to morph button to green circle with icon
MorphingButton btnMorph = (MorphingButton) findViewById(R.id.btnMorph);
// inside on click event
MorphingButton.Params circle = MorphingButton.Params
.create() .duration(500) .cornerRadius(dimen(R.dimen.mb_height_56)) // 56 dp .width(dimen(R.dimen.mb_height_56)) // 56 dp .height(dimen(R.dimen.mb_height_56)) // 56 dp .color(color(R.color.green)) // normal state color .colorPressed(color(R.color.green_dark)) // pressed state color .icon(R.drawable.ic_done); // icon btnMorph.morph(circle);

MorphingButton就是自定義的這個button,裡面有個Params的靜態內部類,設定一些引數如:cornerRadius、width,color等,表示變化到什麼引數,Icon為結束的顯示的圖示。設定好引數後,就呼叫

 public void morph(@NonNull Params params) 

這個方法來執行動畫,使用起來很是簡單。

接下來看下這個庫程式碼的構成,有下面幾個類:

  1. StrokeGradientDrawable.class 這個類就是GradientDrawable就是屬性動畫要變化的物件,在GradientDrawable的基礎上加入了stroke,radius,color的設定,提供了對應set和get方法。

  2. MorphingAnimation.class 這個類就是具體的動畫變化類了。

  3. MorphingButton.class 這個類繼承自button,在程式碼中設定background為StrokeGradientDrawable,這樣對StrokeGradientDrawable做了屬性變化後,動畫效果就顯示在button上了。

就按照這個順序來分析具體的程式碼,先看StrokeGradientDrawable.class:

public class StrokeGradientDrawable {

    private int mStrokeWidth;
    private int mStrokeColor;

    private GradientDrawable mGradientDrawable;
    private float mRadius;
    private int mColor;

    public StrokeGradientDrawable(GradientDrawable drawable) {
        mGradientDrawable = drawable;
    }

    public int getStrokeWidth() {
        return mStrokeWidth;
    }

    public void setStrokeWidth(int strokeWidth) {
        mStrokeWidth = strokeWidth;
        mGradientDrawable.setStroke(strokeWidth, getStrokeColor());
    }

    public int getStrokeColor() {
        return mStrokeColor;
    }

    public void setStrokeColor(int strokeColor) {
        mStrokeColor = strokeColor;
        mGradientDrawable.setStroke(getStrokeWidth(), strokeColor);
    }

    public void setCornerRadius(float radius) {
        mRadius = radius;
        mGradientDrawable.setCornerRadius(radius);
    }

    public void setColor(int color) {
        mColor = color;
        mGradientDrawable.setColor(color);
    }

    public int getColor() {
        return mColor;
    }

    public float getRadius() {
        return mRadius;
    }

    public GradientDrawable getGradientDrawable() {
        return mGradientDrawable;
    }
}

這個類就比較簡單,有mStrokeWidth、mStrokeWidth、mRadius、mColor,這幾個屬性值,還有一個GradientDrawable物件,在建構函式中傳入,然後上面幾個屬性對用的set方法,就是呼叫的GradientDrawable的對應屬性的set方法,剩下的就是get方法。這裡要注意的是:屬性動畫是在變化物件中尋找setXXXX(XXXX即為要變化的屬性)方法來進行變化,所以一定要有對應的set方法

接下來看MorphingAnimation.class,這個類

public class MorphingAnimation {

    //動畫結束的回撥介面
    public interface Listener {
        void onAnimationEnd();
    }

    //內部引數類:變化的button和回撥介面,變化前的屬性和變化後的,屬性有:圓角、高度、寬度、顏色、描邊寬度和顏色
    public static class Params {

        private float fromCornerRadius;
        private float toCornerRadius;

        private int fromHeight;
        private int toHeight;

        private int fromWidth;
        private int toWidth;

        private int fromColor;
        private int toColor;

        private int duration;

        private int fromStrokeWidth;
        private int toStrokeWidth;

        private int fromStrokeColor;
        private int toStrokeColor;

        private MorphingButton button;
        private MorphingAnimation.Listener animationListener;

        private Params(@NonNull MorphingButton button) {
            this.button = button;
        }

        public static Params create(@NonNull MorphingButton button) {
            return new Params(button);
        }

        public Params duration(int duration) {
            this.duration = duration;
            return this;
        }

        public Params listener(@NonNull MorphingAnimation.Listener animationListener) {
            this.animationListener = animationListener;
            return this;
        }

        public Params color(int fromColor, int toColor) {
            this.fromColor = fromColor;
            this.toColor = toColor;
            return this;
        }

        public Params cornerRadius(int fromCornerRadius, int toCornerRadius) {
            this.fromCornerRadius = fromCornerRadius;
            this.toCornerRadius = toCornerRadius;
            return this;
        }

        public Params height(int fromHeight, int toHeight) {
            this.fromHeight = fromHeight;
            this.toHeight = toHeight;
            return this;
        }

        public Params width(int fromWidth, int toWidth) {
            this.fromWidth = fromWidth;
            this.toWidth = toWidth;
            return this;
        }

        public Params strokeWidth(int fromStrokeWidth, int toStrokeWidth) {
            this.fromStrokeWidth = fromStrokeWidth;
            this.toStrokeWidth = toStrokeWidth;
            return this;
        }

        public Params strokeColor(int fromStrokeColor, int toStrokeColor) {
            this.fromStrokeColor = fromStrokeColor;
            this.toStrokeColor = toStrokeColor;
            return this;
        }

    }

    private Params mParams;

    public MorphingAnimation(@NonNull Params params) {
        mParams = params;
    }

    public void start() {
        StrokeGradientDrawable background = mParams.button.getDrawableNormal();

        ObjectAnimator cornerAnimation =
                ObjectAnimator.ofFloat(background, "cornerRadius", mParams.fromCornerRadius, mParams.toCornerRadius);

        ObjectAnimator strokeWidthAnimation =
                ObjectAnimator.ofInt(background, "strokeWidth", mParams.fromStrokeWidth, mParams.toStrokeWidth);

        ObjectAnimator strokeColorAnimation = ObjectAnimator.ofInt(background, "strokeColor", mParams.fromStrokeColor, mParams.toStrokeColor);
        strokeColorAnimation.setEvaluator(new ArgbEvaluator());

        ObjectAnimator bgColorAnimation = ObjectAnimator.ofInt(background, "color", mParams.fromColor, mParams.toColor);
        bgColorAnimation.setEvaluator(new ArgbEvaluator());

        ValueAnimator heightAnimation = ValueAnimator.ofInt(mParams.fromHeight, mParams.toHeight);
        heightAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int val = (Integer) valueAnimator.getAnimatedValue();
                ViewGroup.LayoutParams layoutParams = mParams.button.getLayoutParams();
                layoutParams.height = val;
                mParams.button.setLayoutParams(layoutParams);
            }
        });

        ValueAnimator widthAnimation = ValueAnimator.ofInt(mParams.fromWidth, mParams.toWidth);
        widthAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int val = (Integer) valueAnimator.getAnimatedValue();
                ViewGroup.LayoutParams layoutParams = mParams.button.getLayoutParams();
                layoutParams.width = val;
                mParams.button.setLayoutParams(layoutParams);
            }
        });

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(mParams.duration);
        animatorSet.playTogether(strokeWidthAnimation, strokeColorAnimation, cornerAnimation, bgColorAnimation,
                heightAnimation, widthAnimation);
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (mParams.animationListener != null) {
                    mParams.animationListener.onAnimationEnd();
                }
            }
        });
        animatorSet.start();
    }

}

裡面有一個動畫結束的回撥介面和一個動畫引數的設定內部類,主要看start()方法:
先利用

  StrokeGradientDrawable background = mParams.button.getDrawableNormal();
獲取到button的normal(還有按下狀態)狀態下的background,然後就是利用ObjectAnimator來對這個物件的屬性進行變化,corner、strokeWidth、strokeColor、color這幾個屬性都是類似的,以Corner為例,都是利用:
ObjectAnimator cornerAnimation =
                ObjectAnimator.ofFloat(background, "cornerRadius", mParams.fromCornerRadius, mParams.toCornerRadius);

變化前後的引數就是Params中設定好的引數,然後這裡的width和height變化是利用ValueAnimator,在AnimatorUpdateListener中利用 ViewGroup.LayoutParams,根據產生的變化值動態的設定width和height。最後將這幾個動畫加入到一個AnimatorSet中,來同時顯示,並在最後設定動畫結束後回撥傳入的介面。那麼這個變化前後的引數值是哪裡得到的呢,是在這個類的構造方法中:

  public MorphingAnimation(@NonNull Params params) {
        mParams = params;
    }

最後我們分析MorphingButton.class這個類

public class MorphingButton extends Button {

    private Padding mPadding;
    private int mHeight;
    private int mWidth;
    private int mColor;
    private int mCornerRadius;
    private int mStrokeWidth;
    private int mStrokeColor;

    protected boolean mAnimationInProgress;

    private StrokeGradientDrawable mDrawableNormal;
    private StrokeGradientDrawable mDrawablePressed;

    public MorphingButton(Context context) {
        super(context);
        initView();
    }

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

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (mHeight == 0 && mWidth == 0 && w != 0 && h != 0) {
            mHeight = getHeight();
            mWidth = getWidth();
        }
    }

    public StrokeGradientDrawable getDrawableNormal() {
        return mDrawableNormal;
    }

    public void morph(@NonNull Params params) {
        if (!mAnimationInProgress) {

            mDrawablePressed.setColor(params.colorPressed);
            mDrawablePressed.setCornerRadius(params.cornerRadius);
            mDrawablePressed.setStrokeColor(params.strokeColor);
            mDrawablePressed.setStrokeWidth(params.strokeWidth);

            if (params.duration == 0) {
                morphWithoutAnimation(params);
            } else {
                morphWithAnimation(params);
            }

            mColor = params.color;
            mCornerRadius = params.cornerRadius;
            mStrokeWidth = params.strokeWidth;
            mStrokeColor = params.strokeColor;
        }
    }

    private void morphWithAnimation(@NonNull final Params params) {
        mAnimationInProgress = true;
        setText(null);
        setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
        setPadding(mPadding.left, mPadding.top, mPadding.right, mPadding.bottom);

        MorphingAnimation.Params animationParams = MorphingAnimation.Params.create(this)
                .color(mColor, params.color)
                .cornerRadius(mCornerRadius, params.cornerRadius)
                .strokeWidth(mStrokeWidth, params.strokeWidth)
                .strokeColor(mStrokeColor, params.strokeColor)
                .height(getHeight(), params.height)
                .width(getWidth(), params.width)
                .duration(params.duration)
                .listener(new MorphingAnimation.Listener() {
                    @Override
                    public void onAnimationEnd() {
                        finalizeMorphing(params);
                    }
                });

        MorphingAnimation animation = new MorphingAnimation(animationParams);
        animation.start();
    }

    private void morphWithoutAnimation(@NonNull Params params) {
        mDrawableNormal.setColor(params.color);
        mDrawableNormal.setCornerRadius(params.cornerRadius);
        mDrawableNormal.setStrokeColor(params.strokeColor);
        mDrawableNormal.setStrokeWidth(params.strokeWidth);

        if(params.width != 0 && params.height !=0) {
            ViewGroup.LayoutParams layoutParams = getLayoutParams();
            layoutParams.width = params.width;
            layoutParams.height = params.height;
            setLayoutParams(layoutParams);
        }

        finalizeMorphing(params);
    }

    private void finalizeMorphing(@NonNull Params params) {
        mAnimationInProgress = false;

        if (params.icon != 0 && params.text != null) {
            setIconLeft(params.icon);
            setText(params.text);
        } else if (params.icon != 0) {
            setIcon(params.icon);
        } else if(params.text != null) {
            setText(params.text);
        }

        if (params.animationListener != null) {
            params.animationListener.onAnimationEnd();
        }
    }

    public void blockTouch() {
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return true;
            }
        });
    }

    public void unblockTouch() {
        setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return false;
            }
        });
        invalidate();
    }

    private void initView() {
        mPadding = new Padding();
        mPadding.left = getPaddingLeft();
        mPadding.right = getPaddingRight();
        mPadding.top = getPaddingTop();
        mPadding.bottom = getPaddingBottom();

        Resources resources = getResources();
        int cornerRadius = (int) resources.getDimension(R.dimen.mb_corner_radius_2);
        int blue = resources.getColor(R.color.mb_blue);
        int blueDark = resources.getColor(R.color.mb_blue_dark);

        StateListDrawable background = new StateListDrawable();
        mDrawableNormal = createDrawable(blue, cornerRadius, 0);
        mDrawablePressed = createDrawable(blueDark, cornerRadius, 0);

        mColor = blue;
        mStrokeColor = blue;
        mCornerRadius = cornerRadius;

        background.addState(new int[]{android.R.attr.state_pressed}, mDrawablePressed.getGradientDrawable());
        background.addState(StateSet.WILD_CARD, mDrawableNormal.getGradientDrawable());

        setBackgroundCompat(background);
    }

    private StrokeGradientDrawable createDrawable(int color, int cornerRadius, int strokeWidth) {
        StrokeGradientDrawable drawable = new StrokeGradientDrawable(new GradientDrawable());
        drawable.getGradientDrawable().setShape(GradientDrawable.RECTANGLE);
        drawable.setColor(color);
        drawable.setCornerRadius(cornerRadius);
        drawable.setStrokeColor(color);
        drawable.setStrokeWidth(strokeWidth);

        return drawable;
    }

    @SuppressWarnings("deprecation")
    private void setBackgroundCompat(@Nullable Drawable drawable) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN) {
            setBackgroundDrawable(drawable);
        } else {
            setBackground(drawable);
        }
    }

    public void setIcon(@DrawableRes final int icon) {
        // post is necessary, to make sure getWidth() doesn't return 0
        post(new Runnable() {
            @Override
            public void run() {
                Drawable drawable = getResources().getDrawable(icon);
                int padding = (getWidth() / 2) - (drawable.getIntrinsicWidth() / 2);
                setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0);
                setPadding(padding, 0, 0, 0);
            }
        });
    }

    public void setIconLeft(@DrawableRes int icon) {
        setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0);
    }

首先在構造方法中呼叫了initView()方法,建立一個StateListDrawable物件,然後利用

 private StrokeGradientDrawable createDrawable(int color, int cornerRadius, int strokeWidth)

產生一個StrokeGradientDrawable 物件,在利用

  background.addState(new int[]{android.R.attr.state_pressed}, mDrawablePressed.getGradientDrawable());
        background.addState(StateSet.WILD_CARD, mDrawableNormal.getGradientDrawable());

        setBackgroundCompat(background);
 分別設定了按下裝填和普通狀態的背景,當前的mColor,mStrokeColor,mCornerRadius,就是對用Params中變化前的引數,然後看下morph()這個方法,這個方法就是最開始使用的方法:開始進行變換,在其中呼叫了morphWithAnimation(@NonNull final Params params) 方法,來執行具體的動畫,這個方法中傳入的params就是使用這個庫時,建立的param,代表變化後的引數,是MorphingButton類中的一個內部類,上面程式碼中沒有貼出來,然後根據變化前的引數和傳入的變化的param構造 MorphingAnimation.Params這個引數,就是變化前和變化後的引數animationParams,最後利用
  MorphingAnimation animation = new MorphingAnimation(animationParams);
        animation.start();
在建立MorphingAnimation 時,將這個animationParams引數傳入,呼叫start()方法開始動畫。可以看到在動畫結束後的回撥介面中呼叫了
 finalizeMorphing(params);

這個方法裡面有個 setIconLeft(params.icon),setText()來設定動畫結束後的顯示的圖示和文字,需要注意的是在setIconLeft()方法中,是利用:

  // post is necessary, to make sure getWidth() doesn't return 0
 post(new Runnable() {
            @Override
            public void run() {
                Drawable drawable = getResources().getDrawable(icon);
                int padding = (getWidth() / 2) - (drawable.getIntrinsicWidth() / 2);
                setCompoundDrawablesWithIntrinsicBounds(icon, 0, 0, 0);
                setPadding(padding, 0, 0, 0);
            }
        });

setCompoundDrawablesWithIntrinsicBounds()方法來設定ICON,並且設定一個padding來留出ICON的位置,這裡使用的Post(Runnable runbable)方法是為了避免獲取獲取的getWidth()為0,。

相信說到這裡,應該已經明白第一個動畫效果是怎麼實現的,其實關於button的動畫大多數是應該對background的drawable做變換,這個庫程式碼我覺得寫得還是不錯的,看著比較清晰,對於設定引數這塊的程式碼還是寫得挺好的,,在外部呼叫很直觀。

但這只是一個最簡單的動畫效果,還有一個更加酷炫的動畫效果:

這裡寫圖片描述

這個動畫效果和第二個動畫效果放到下一篇部落格中進行解析。