前言:开发当中,一般属性动画的缩放、平移、淡出、旋转,可以解决大部分需求,但是如果App本身对动画要求较高需要自定义动画移动路径,或者速率,则对Interpolator,TyperEvaluator了解便必不可少。 如:想实现如下图动画效果 缩放、平移、淡出、旋转便显得不足了。
TimeInterpolator
TypeEvaluator
官网的介绍,英文不好,就不献丑了。 这里我按我的理解简单解释一下
用于控制动画的变换速率,如:小球从A点到B点是加速运动还是减速运动,便是由它来控制。 它是一个接口,所有的Interpolator都要实现这个接口, 它只有一个返回float值 getInterpolation(float input)方法, 方法中的 input 是系统根据设置的 duration 经过计算后传入到这里来的,从0匀速增加到1的值。 如:设置了一个3s的 duration 和 1s的 duration 变化都是由 0 到1,只不过速率不一样
用于控制动画如何从开始过渡到结束的,如:A(0,0) B(0,5)过两点之间的线可以是一条直线,也能是一条曲线,这个便由TypeEvaluator控制。 它是一个接口,只有一个返回泛型evaluate(float fraction, T startValue, T endValue); 方法 ,第一个参数是 TimeInterpolator中的getInterpolation(float input) 计算完成之后的返回值,第二、三个参数,就如字面意思,开始值和结束值,值得注意的是它返回的是一个Object(泛型会被擦除),这意味着它开始点和结束点可以由多个系数决定。 如:可以传入直角坐标系中的x,y 也可以传入空间直角坐标系中的 x,y,z 等等。
说了这么多,可能理解还是有些晦涩,那么,实践、实践!
TyperEvaluatorde 的作用是用于控制动画如何过渡的 如图,我想控制小球由屏幕中间的上方移动到屏幕右下角。 先看代码:
/** * 这个方法hasFocus true表示页面绘制完成,这样 view.getWidth 之类的方法就不会返回 0 * @param hasFocus */ @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if(hasFocus){ init(); } } private void init() { int width = rootView.getWidth(); int height = rootView.getHeight(); PointF pointF1 = new PointF(rootView.getWidth()/2, 0); PointF pointF2 = new PointF(width - imgBall.getWidth(), height - imgBall.getHeight()); ValueAnimator valueAnimator = new ValueAnimator(); valueAnimator.setObjectValues(pointF1, pointF2); valueAnimator.setEvaluator(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { PointF startPoint = (PointF) startValue; PointF endPoint = (PointF) endValue; float x = startPoint.x + fraction * (endPoint.x - startPoint.x); float y = startPoint.y + fraction * (endPoint.y - startPoint.y); PointF point = new PointF(x, y); Log.d("TypeEvaluator","fraction" + fraction + "\n startPoint.x = "+ startPoint.x +"startPoint.y = " + startPoint.y +"\nx = " + x+ "__y = "+ y); return point; } }); valueAnimator.setDuration(5000); valueAnimator.start(); //这里并没有给 View 设置动画,而是监听动画变化值,不断更新View 的位置, valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { PointF pointF = (PointF) animation.getAnimatedValue(); imgBall.setX(pointF.x); imgBall.setY(pointF.y); } }); }关键代码:valueAnimator.setEvaluator()方法,我设置了一个匿名内部类用于实现TypeEvaluator,仔细看肯定注意到了 log.d 语句,上图! 这里的开始点是 (rootView.getWidth()/2, 0) 结束点是(width - imgBall.getWidth(), height - imgBall.getHeight()) 其实就是屏幕最上方中间,到屏幕右下角 fraction 由Interpolator的返回值决定, 按照公式自己套几个值进去TypeEvaluator 的作用此时应该是明明白白了。
注意:TypeEvaluator的 evaluate方法中的的三个参数开始和结束值都是固定的,对应的是动画开始位置和结束位置,fraction这个参数是TimeInterpolator中getInterpolation(float input) 计算完成之后的返回值;而这个input和动画的duration有关。
InterpolatorDemo
Interpolator用于控制动画的变化速率,如图我想控制小球从开始点到结束点做减速运动 代码:
/** * 这个方法hasFocus true表示页面绘制完成,这样 view.getWidth 之类的方法就不会返回 0 * @param hasFocus */ @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if(hasFocus){ init(); } } private void init() { Interpolator line = new LinearInterpolator();//线性 Interpolator acc = new AccelerateInterpolator();//加速 Interpolator dce = new DecelerateInterpolator();//减速 Interpolator accdec = new AccelerateDecelerateInterpolator();//先加速后减速 int width = rootView.getWidth(); int height = rootView.getHeight(); PointF pointF1 = new PointF(rootView.getWidth()/2, 0); PointF pointF2 = new PointF(width / 2 - imgBall.getWidth(), height - imgBall.getHeight()); ValueAnimator valueAnimator = new ValueAnimator(); valueAnimator.setObjectValues(pointF1, pointF2); valueAnimator.setInterpolator(dce); //设置插值器 默认是 LinearInterpolator(线性); valueAnimator.setEvaluator(new TypeEvaluator() { @Override public Object evaluate(float fraction, Object startValue, Object endValue) { PointF startPoint = (PointF) startValue; PointF endPoint = (PointF) endValue; float x = startPoint.x + fraction * (endPoint.x - startPoint.x); float y = startPoint.y + fraction * (endPoint.y - startPoint.y); PointF point = new PointF(x, y); Log.d("TypeEvaluator","fraction" + fraction); return point; } }); valueAnimator.setDuration(1000); valueAnimator.start(); //这里并没有给 View 设置动画,而是监听动画变化值,不断更新View 的位置, valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { PointF pointF = (PointF) animation.getAnimatedValue(); imgBall.setX(pointF.x); imgBall.setY(pointF.y); } }); }这里我使用了系统提供的Interpolator DecelerateInterpolator
public float getInterpolation(float input) { float result; if (mFactor == 1.0f) { result = (float)(1.0f - (1.0f - input) * (1.0f - input)); } else { result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor)); } return result; }看见这些公式我就头疼····,还是那句话 fraction是 TimeInterpolator中getInterpolation(float input) 计算完成之后的返回值,要查看这个result只需要去TypeEvaluator中的fraction 值就好了. 具体我就不多说了,自己看数据吧。
东西弄清楚了,不弄几个东西实践一下总觉得有种拔剑四顾心茫然之感~ 下面我说明一下,这些Demo都是网上找的,看着效果自己实践了一遍,而效果的真正作者我也不知道是谁了~ 能确定的只有damajia,所以一下借鉴的代码我就不说明出处了,如作者深究可联系我。 整个项目源码会在文章末尾放出来,所以下面的Demo我只贴关键代码了。
这个动画和本文无关,去网上搜索看见这动画,感觉思路很有意思所以贴了出来。 刚开始看着效果的时候说实话有点懵逼,不过看见下面那个图我想肯定会一拍大腿~。
private void starAnim() { ObjectAnimator ballAnim1 = ObjectAnimator.ofFloat(llPointCircle1, "rotation", 0, 360); ballAnim1.setDuration(2000); ballAnim1.setInterpolator(new AccelerateDecelerateInterpolator()); ObjectAnimator ballAnim2 = ObjectAnimator.ofFloat(llPointCircle2, "rotation", 0, 360); ballAnim2.setStartDelay(150); ballAnim2.setDuration(2000); ballAnim2.setInterpolator(new AccelerateDecelerateInterpolator()); ObjectAnimator ballAnim3 = ObjectAnimator.ofFloat(llPointCircle3, "rotation", 0, 360); ballAnim3.setStartDelay(2 * 150); ballAnim3.setDuration(2000); ballAnim3.setInterpolator(new AccelerateDecelerateInterpolator()); ObjectAnimator ballAnim4 = ObjectAnimator.ofFloat(llPointCircle4, "rotation", 0, 360); ballAnim4.setStartDelay(3 * 150); ballAnim4.setDuration(2000); ballAnim4.setInterpolator(new AccelerateDecelerateInterpolator()); AnimatorSet animatorSet = new AnimatorSet(); animatorSet.play(ballAnim1).with(ballAnim2).with(ballAnim3).with(ballAnim4); animatorSet.start(); }它其实就是一个组合动画,然后给4个ViewAnim加了一个延时执行。
接着来看来看大神daimajia的一个动画开源项目
private void setDropout(){ int distance = tvView.getTop() + tvView.getHeight(); AnimatorSet animSet = new AnimatorSet(); ObjectAnimator alpha = ObjectAnimator.ofFloat(tvView, "alpha", 0, 1); BounceEaseOut b = new BounceEaseOut(1000); ValueAnimator dropout = ObjectAnimator.ofFloat(tvView, "translationY", -distance, 0); dropout.setEvaluator(b); animSet.playTogether(alpha,dropout); animSet.setDuration(1200); animSet.start(); }这个其实是一个组合动画,一个透明和一下下坠动画,关键是这个下坠动画来看BounceEaseOut的代码
public class BounceEaseOut implements TypeEvaluator<Number> { private float mDuration; public BounceEaseOut(float mDuration) { this.mDuration = mDuration; } @Override public Number evaluate(float fraction, Number startValue, Number endValue) { float t = mDuration * fraction; float b = startValue.floatValue(); float c = endValue.floatValue() - startValue.floatValue(); float d = mDuration; if ((t/=d) < (1/2.75f)) { return c*(7.5625f*t*t) + b; } else if (t < (2/2.75f)) { return c*(7.5625f*(t-=(1.5f/2.75f))*t + .75f) + b; } else if (t < (2.5/2.75)) { return c*(7.5625f*(t-=(2.25f/2.75f))*t + .9375f) + b; } else { return c*(7.5625f*(t-=(2.625f/2.75f))*t + .984375f) + b; } } }更多的效果见https://github.com/daimajia/AndroidViewAnimations 这段公式大致意思是到一个时间点把View位置移动到终点,然后设置反复回弹… 看见这计算惊喜不惊喜,意外不意外? 反正我高中数学早就丢给老师了,不过~
···搞了半天总于到这里了,我们做动画的时候常常会看见什么贝塞尔曲线,二阶、三阶、四阶balabala··· OK 来看看这个Bezier到底是啥玩意。
贝塞尔曲线 贝塞尔曲线就是这样的一条曲线,它是依据四个位置任意的点坐标绘制出的一条光滑曲线。在历史上,研究贝塞尔曲线的人最初是按照已知曲线参数方程来确定四个点的思路设计出这种矢量曲线绘制法。贝塞尔曲线的有趣之处更在于它的“皮筋效应”,也就是说,随着点有规律地移动,曲线将产生皮筋伸引一样的变换,带来视觉上的冲击。1962年,法国数学家Pierre Bézier第一个研究了这种矢量绘制曲线的方法,并给出了详细的计算公式,因此按照这样的公式绘制出来的曲线就用他的姓氏来命名是为贝塞尔曲线。
···说白了它就是一个公式,分了很多阶。
一阶
二阶
更多自行百度 Google~
下面看下用这个公式套入TypeEvaluator会有些什么效果
在购物车中,这种动画应该是很常见的。
public class BezierEvaluator implements TypeEvaluator<Point> { private Point controllPoint; public BezierEvaluator(Point controllPoint) { this.controllPoint = controllPoint; } @Override public Point evaluate(float t, Point startValue, Point endValue) { //二阶Bezier int x = (int) ((1 - t) * (1 - t) * startValue.x + 2 * t * (1 - t) * controllPoint.x + t * t * endValue.x); int y = (int) ((1 - t) * (1 - t) * startValue.y + 2 * t * (1 - t) * controllPoint.y + t * t * endValue.y); return new Point(x, y); } }这里大致说一下思路,整个是一个RecyclerView 然后给每一个条目的图片一个点击事件,每一次点击获取到条目上的图片的位置,然后把整个位置设置为startValue, 而结束值是底部那个图片的位置,整个动画关键在于这个 BezierEvaluator 没啥好说的,照着二阶的Bezier公式套就好了,每一次会返回Point的位置,无数个Point不断变化就是我们看见的动画了。
下面是一个综合起来的例子,一个点赞效果。
/** * 使用 bubbingLayout.setHeart(); */ public class BubbingLayout extends RelativeLayout { private Interpolator line = new LinearInterpolator();//线性 private Interpolator acc = new AccelerateInterpolator();//加速 private Interpolator dce = new DecelerateInterpolator();//减速 private Interpolator accdec = new AccelerateDecelerateInterpolator();//先加速后减速 private Interpolator[] interpolators; private int mHeight; private int mWidth; private LayoutParams lp; private Drawable[] drawables; private Random random = new Random(); private int dHeight; private int dWidth; public BubbingLayout(Context context) { super(context); init(); } public BubbingLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public BubbingLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { //初始化小球 drawables = new Drawable[3]; Drawable red = getResources().getDrawable(R.drawable.red_ball); Drawable yellow = getResources().getDrawable(R.drawable.yellow_ball); Drawable blue = getResources().getDrawable(R.drawable.blue_ball); drawables[0] = red; drawables[1] = yellow; drawables[2] = blue; //获取图的宽高 用于后面的计算 //注意 这里由于图片的大小都是一样的,所以只取了一个 dHeight = red.getIntrinsicHeight(); dWidth = red.getIntrinsicWidth(); //位置 底部 并且 水平居中 lp = new LayoutParams(dWidth, dHeight); lp.addRule(CENTER_HORIZONTAL, TRUE); lp.addRule(ALIGN_PARENT_BOTTOM, TRUE); // 初始化插值器 interpolators = new Interpolator[4]; interpolators[0] = line; interpolators[1] = acc; interpolators[2] = dce; interpolators[3] = accdec; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); } public void setHeart() { ImageView imageView = new ImageView(getContext()); //随机一个小球 imageView.setImageDrawable(drawables[random.nextInt(3)]); imageView.setLayoutParams(lp); addView(imageView); Animator set = getAnimator(imageView); set.addListener(new AnimEndListener(imageView)); set.start(); } private Animator getAnimator(View target) { AnimatorSet set = getEnterAnimtor(target); ValueAnimator bezierValueAnimator = getBezierValueAnimator(target); AnimatorSet finalSet = new AnimatorSet(); finalSet.playSequentially(set); finalSet.playSequentially(set, bezierValueAnimator); finalSet.setInterpolator(interpolators[random.nextInt(4)]); finalSet.setTarget(target); return finalSet; } /** * 开始动画 先由暗到明 并且 中心放大 * @param target * @return */ private AnimatorSet getEnterAnimtor(final View target) { ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f); ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f); ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f); AnimatorSet enter = new AnimatorSet(); enter.setDuration(500); enter.setInterpolator(new LinearInterpolator()); enter.playTogether(alpha, scaleX, scaleY); enter.setTarget(target); return enter; } private ValueAnimator getBezierValueAnimator(View target) { //贝塞尔计算器 传入三阶公式的必要参数 ( 由于起点和终点已确定,这里控制曲线则由中间2个点来控制 ) HeartBezierEvaluator evaluator = new HeartBezierEvaluator(getPointF(2), getPointF(1)); PointF startPoint = new PointF((mWidth - dWidth) / 2, mHeight - dHeight); PointF endPoint = new PointF(random.nextInt(getWidth()), 0); // 第三个参数是起点 第四个参数是终点 ValueAnimator animator = ValueAnimator.ofObject(evaluator, startPoint,endPoint); animator.addUpdateListener(new BezierListener(target)); animator.setTarget(target); animator.setDuration(3000); return animator; } /** * 获取中间的两个 点 * * @param scale */ private PointF getPointF(int scale) { PointF pointF = new PointF(); pointF.x = random.nextInt((mWidth - 100));//减去100 是为了控制 x轴活动范围,看效果 随意~~ //再Y轴上 为了确保第二个点 在第一个点之上,我把Y分成了上下两半 这样动画效果好一些 也可以用其他方法 pointF.y = random.nextInt((mHeight - 100)) / scale; return pointF; } private class BezierListener implements ValueAnimator.AnimatorUpdateListener { private View target; public BezierListener(View target) { this.target = target; } @Override public void onAnimationUpdate(ValueAnimator animation) { //获得曲线路径点的值,不断更新View的位置 PointF pointF = (PointF) animation.getAnimatedValue(); target.setX(pointF.x); target.setY(pointF.y); // 由明到暗的动画, 注意 fraction 值是 一个由 0 到 1 的值。 target.setAlpha(1 - animation.getAnimatedFraction()); } } private class AnimEndListener extends AnimatorListenerAdapter { private View target; public AnimEndListener(View target) { this.target = target; } @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); //移除View removeView((target)); } } }大致说一下,整个动画曲线是由三阶Bezier控制的,起点固定,终点的y值固定x值是Layou宽度的一个随机值。每次曲线以及速率随机生成(也就是TypeEvaluator和Interpolator 是随机的),随机范围可以看代码,并且自下而上添加了一个透明动画。 代码关键地方都有注释,就不过多啰嗦了。
总结:TypeEvanluator和Interpolator是息息相关的,一个控制动画的过渡方式,一个控制动画的过度速率,动画的过渡路径可以借助Beizer公式实现一些比较好看的曲线,而过渡速率系统已经提供了比较丰富的实现了(如果非要自己实现,可参考系统的计算公式~)
源码 https://github.com/FmrChina/AnimaDemo
好了属性动画到这里应该可以满足绝大部分开发需求了,自己想实现一些比较酷的动画,只要有耐心以及数学功底够,应该也是不难的了,不过话说回来~数学是硬伤!
这里提下个人认为的动画知识体系 动画我大致分为了三类 - 页面元素动画 - 页面切换动画 - 以及ViewPager或ReccyclerView加载动画
这篇文章对应的便是页面元素动画。
