一个酷炫的button变化动画开源库源码分析—Android morph Button(一)

来源:互联网 发布:做照片的软件 编辑:程序博客网 时间:2024/05/19 11:48

最近很是喜爱一些酷炫的动画效果,特意在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 iconMorphingButton btnMorph = (MorphingButton) findViewById(R.id.btnMorph);// inside on click eventMorphingButton.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); // iconbtnMorph.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做变换,这个库代码我觉得写得还是不错的,看着比较清晰,对于设置参数这块的代码还是写得挺好的,,在外部调用很直观。

但这只是一个最简单的动画效果,还有一个更加酷炫的动画效果:

这里写图片描述

这个动画效果和第二个动画效果放到下一篇博客中进行解析。

1 0
原创粉丝点击