[关闭]
@TryLoveCatch 2022-05-17T10:35:39.000000Z 字数 11962 阅读 1373

Android知识体系之动画

Android知识体系


属性动画,默认持续时间为300ms,默认帧率为10ms/帧

属性动画

HenCoder Android 自定义 View 1-6:属性动画 Property Animation(上手篇)

  • ObjectAnimator的translationX和translationY动画,传入的值,都是基于View本身的位置来算的
  • 在xml里面,android:translationX和android:translationY来设置view的位置

我们可以将TimeInterpolator和TypeEvaluator看作是工厂流水线上的两个小员工,那么ValueAnimator就是车间主管啦。TimeInterpolator这个小员工面对的是产品的半成品,他负责控制半成品输出到下一个生产线的速度。而下一个生产线上的小员工TypeEvaluator的任务就是打磨半成品得到成品,最后将成品输出。

插值器(TimeInterpolator)&估值器(TypeEvaluator)

插值器:

TimeInterpolator,根据时间流逝的百分比,来计算当前属性值需要改变的百分比。

  1. public interface TimeInterpolator {
  2. float getInterpolation(float input);
  3. }

估值器:

TypeEvaluator,根据当前属性需要改变的百分比,来计算改变后的属性值。

  1. public interface TypeEvaluator<T> {
  2. public T evaluate(float fraction, T startValue, T endValue);
  3. }

它们两个决定了动画的变化过程,举个例子说明:


如上图所示,它表示一个匀速动画,采用LinearInterpolatorIntEvaluator,在40ms内,Viewx属性从040的变化。

我们来分析一下,由于默认帧率为10ms/帧,所以这个动画,应该有5帧,我们来看第三帧是如何计算出x=20的。当t=20ms时,时间流逝的百分比就是0.5(20/40=0.5),然后我们根据插值器来计算属性x需要改变的百分比,由于我们用的是LinearInterpolator,源码如下:

  1. public class LinearInterpolator implements Interpolator {
  2. public LinearInterpolator() {
  3. }
  4. public float getInterpolation(float input) {
  5. return input;
  6. }
  7. }

所以属性x需要改变的百分比也是0.5,这样插值器的作用就结束了,接下来就是估值器,来得到属性x具体的值,我们使用的是IntEvaluator,源码如下:

  1. public class IntEvaluator implements TypeEvaluator<Integer> {
  2. public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
  3. int startInt = startValue;
  4. return (int)(startInt + fraction * (endValue - startInt));
  5. }
  6. }

具体来说下evaluate()的三个参数:

fraction:估值小数,就是我们在插值器里面计算的结果,这里就是0.5
startValue:开始值,这里就是0
endValue:结束值,这里就是40

所以,根据这个方法的算法,0+0.5*(40-0)=20,得到x=20

TimeInterpolator

当数学遇上动画:讲述 ValueAnimator、TypeEvaluator 和 TimeInterpolator 之间的恩恩怨怨 (1)

TimeInterpolator,时间插值器,根据时间流逝的百分比计算出当前属性值改变的百分比。

TimeInterpolator

  1. public interface TimeInterpolator {
  2. float getInterpolation(float input);
  3. }

这里面还有一个接口Interpolator

  1. public interface Interpolator extends TimeInterpolator {
  2. }

Interpolator竟然是继承TimeInterpolator,是不是有点奇怪?
其实,是这样的:

从Android 1.0版本开始就一直存在Interpolator接口了,而之前的补间动画当然也是支持这个功能的。只不过在属性动画中新增了一个TimeInterpolator接口,这个接口是用于兼容之前的Interpolator的,这使得所有过去的Interpolator实现类都可以直接拿过来放到属性动画当中使用

我们来看回TimeInterpolator,里面只有一个方法

  1. float getInterpolation(float input);

input参数是由系统计算传入的,变化很有规律,就是根据动画的时长从0到1匀速增加。
input指的是真实的时间,如果动画的总时长是10s,已经过了4s了,那么input=0.4,所以返回值按理说,取值范围是[0,1],0表示动画开始,1表示动画结束,但是也可以小于0(”下冲”)或者大于1(”上冲”)。

我们都知道时间是一秒一秒过去的,也就是线性的,匀速前进的。如果属性值从起始值到结束值是匀速变化的话,那么整个动画看起来就是慢慢地均匀地变化着。但是,如果我们想让动画变得很快或者变得很慢怎么办?答案是我们可以通过“篡改时间”来完成这个任务!这正是TimeInterpolator类的工作,它实际上就是一条函数曲线。

举个栗子!如下图所示,x轴表示时间的比率,y轴表示属性值。在不考虑TypeEvaluator的计算的情况下,假设属性值是从0变化到1,默认情况下线性插值器就和曲线y=x一样,在时间t的位置上的值为f(t)=t,当t=0.5的时刻传给TypeEvaluator的是t=0.5的时刻的值0.5。但是,当我们将TimeInterpolator设置为函数y=x^2或者y=x^(0.5)时,动画的效果就截然不同啦。在t=0.5的时刻,y=x^2=0.25 < 0.5,表示它将时间推迟了,传给TypeEvaluator的是0.25时刻的值0.25;而y=x^(0.5)=0.71 > 0.5,表示它将时间提前了,传给TypeEvaluator的是0.71时刻的值0.71。

仔细观察曲线的斜率不难发现,曲线y=x^2的斜率在不断增加,说明变化越来越快,作用在View组件上就是刚开始挺慢的,然后不断加速的效果;而曲线y=x^(0.5)的斜率在不断减小,说明变化越来越慢,作用在View组件上就是刚开始挺快的,然后不断减速的效果。

TypeEvaluator

TypeEvaluator,类型估值算法,根据当前属性改变的百分比来计算改变后的属性值。

TypeEvaluator

  1. public interface TypeEvaluator<T> {
  2. public T evaluate(float fraction, T startValue, T endValue);
  3. }

TypeEvaluator实际上也是一条函数曲线,它的输入是TimeInterpolator传进来的被“篡改”了的时间比率,还有动画的起始值和结束值信息,输出就是动画当前应该更新的属性值。假设TimeInterpolator是LinearInterpolator(f(t)=t),也就是说时间比率不被“篡改”的话,那么ValueAnimator对应的函数其实就简化成了TypeEvaluator函数(F=g(x,a,b)=g(f(t),a,b)=g(t,a,b)),即动画实际上只由TypeEvaluator来控制。

ValueAnimator

假设TimeInterpolator是x=f(t),而TypeEvaluator的函数是y=g(x,a,b),那么,结合而成的复合函数 F=g(f(t),a,b),就是ValueAnimator

TimeInterpolator和TypeEvaluator关系

TimeInterpolator是用来控制动画速度的,而TypeEvaluator是用来控制动画中值的变化曲线的。
虽然它们本质的作用是不同的,但是在某些定制的情况下,上面说的两种特殊情况下的ValueAnimator所产生的动画效果是一样的!

也就是说,为了实现某一个动画,我们可以单独使用TimeInterpolator,或者单独使用TypeEvaluator,都可以成功实现。

举个例子

实现在1s中内将float类型的数值从0变化到1。

  1. ValueAnimator animator1 = new ValueAnimator();
  2. animator1.setFloatValues(0.0f, 1.0f);
  3. animator1.setDuration(1000);
  4. animator1.setInterpolator(new LinearInterpolator());//传入null也是LinearInterpolator
  5. animator1.setEvaluator(new TypeEvaluator() {
  6. @Override
  7. public Object evaluate(float fraction, Object startValue, Object endValue) {
  8. return startValue + fraction * (endValue - startValue);
  9. }
  10. });
  11. animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  12. @Override
  13. public void onAnimationUpdate(ValueAnimator animation) {
  14. Log.e("demo 1", "" + animation.getAnimatedValue());
  15. }
  16. });
  17. // =================================================================
  18. ValueAnimator animator2 = new ValueAnimator();
  19. animator2.setFloatValues(0.0f, 1.0f);
  20. animator2.setDuration(1000);
  21. animator2.setInterpolator(new Interpolator() {
  22. @Override
  23. public float getInterpolation(float input) {
  24. return input;
  25. }
  26. });
  27. animator2.setEvaluator(new TypeEvaluator() {
  28. @Override
  29. public Object evaluate(float fraction, Object startValue, Object endValue) {
  30. return fraction;
  31. }
  32. });
  33. animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  34. @Override
  35. public void onAnimationUpdate(ValueAnimator animation) {
  36. Log.e("demo 2", "" + animation.getAnimatedValue());
  37. }
  38. });
  39. animator1.start();
  40. animator2.start();

两个ValueAnimator的在真实时间序列中的输出结果是一样的,所以说,在特殊的单独控制动画的情况下,TimeInterpolator和TypeEvaluator对于制作动画有着殊途同归的作用。
当然,你可以同时使用自定义的TimeInterpolator和自定义的TypeEvaluator结合来控制动画,但是很显然,这种情况下的动画不容易控制。
从数学中函数的角度上来说那就是复合函数肯定比简单函数复杂,我们解决问题的时候要可能化繁为简,所以自然会考虑将ValueAnimator这个复杂函数简化成特殊情况下的简单函数TimeInterpolator或者TypeEvaluator来处理。

统一处理

上面说了,TimeInterpolator和TypeEvaluator对于制作动画有着殊途同归的作用。而且上面的例子,我们也看出来了,自定义imeInterpolator和自定义TypeEvaluator的代码基本上是一样的,那么我们是不是可以把这两个进行统一处理呢?

yava,这个项目就实现了这个需求,我们来看一下它里面3个重要的类。

IFunction接口

  1. /**
  2. * 函数接口:给定输入,得到输出
  3. */
  4. public interface IFunction {
  5. float getValue(float input);
  6. }

AbstractFunction抽象类

  1. /**
  2. * 抽象函数实现
  3. */
  4. public abstract class AbstractFunction implements IFunction, Interpolator, TypeEvaluator<Float> {
  5. @Override
  6. public float getInterpolation(float input) {
  7. return getValue(input);
  8. }
  9. @Override
  10. public Float evaluate(float fraction, Float startValue, Float endValue) {
  11. return startValue + getValue(fraction) * (endValue - startValue);
  12. }
  13. }

这个类,既可以当做简单函数使用,也可以当做Interpolator或者TypeEvaluator去用于制作动画。

EasingFunction枚举:包含了30个常见的缓动函数

  1. public enum EasingFunction implements IFunction, Interpolator, TypeEvaluator<Float> {
  2. /* ------------------------------------------------------------------------------------------- */
  3. /* BACK
  4. /* ------------------------------------------------------------------------------------------- */
  5. BACK_IN {
  6. @Override
  7. public float getValue(float input) {
  8. return input * input * ((1.70158f + 1) * input - 1.70158f);
  9. }
  10. },
  11. BACK_OUT {
  12. @Override
  13. public float getValue(float input) {
  14. return ((input = input - 1) * input * ((1.70158f + 1) * input + 1.70158f) + 1);
  15. }
  16. },
  17. BACK_INOUT {
  18. @Override
  19. public float getValue(float input) {
  20. float s = 1.70158f;
  21. if ((input *= 2) < 1) {
  22. return 0.5f * (input * input * (((s *= (1.525f)) + 1) * input - s));
  23. }
  24. return 0.5f * ((input -= 2) * input * (((s *= (1.525f)) + 1) * input + s) + 2);
  25. }
  26. },
  27. //other easing functions ......
  28. //如果这个function在求值的时候需要duration作为参数的话,那么可以通过setDuration来设置,否则使用默认值
  29. private float duration = 1000f;//目前只有ELASTIC***这三个是需要duration的,其他的都不需要
  30. public float getDuration() {
  31. return duration;
  32. }
  33. public EasingFunction setDuration(float duration) {
  34. this.duration = duration;
  35. return this;
  36. }
  37. //将Function当做Interpolator使用,默认的实现,不需要枚举元素去重新实现
  38. @Override
  39. public float getInterpolation(float input) {
  40. return getValue(input);
  41. }
  42. //将Function当做TypeEvaluator使用,默认的实现,不需要枚举元素去重新实现
  43. @Override
  44. public Float evaluate(float fraction, Float startValue, Float endValue) {
  45. return startValue + getValue(fraction) * (endValue - startValue);
  46. }
  47. //几个数学常量
  48. public static final float PI = (float) Math.PI;
  49. public static float TWO_PI = PI * 2.0f;
  50. public static float HALF_PI = PI * 0.5f;
  51. }

注意,枚举跟抽象方法配合使用。
这个类提供类常见的30个缓动函数的实现,类似于工具类,方便使用。

例子
  1. ObjectAnimator animator1 = new ObjectAnimator();
  2. animator1.setTarget(textView1);
  3. animator1.setPropertyName("translationY");
  4. animator1.setFloatValues(0f, -100f); animator1.setDuration(1000);
  5. animator1.setInterpolator(new LinearInterpolator());
  6. animator1.setEvaluator(EasingFunction.BOUNCE_OUT);//这里将EasingFunction.BOUNCE_OUT作为TypeEvaluator来使用
  7. animator1.start();
  1. ObjectAnimator animator2 = new ObjectAnimator();
  2. animator2.setTarget(textView2);
  3. animator2.setPropertyName("translationY");
  4. animator2.setFloatValues(0f, -100f);
  5. animator2.setDuration(1000);
  6. animator2.setInterpolator(EasingFunction.BOUNCE_OUT); //这里将EasingFunction.BOUNCE_OUT作为Interpolator来使用
  7. animator2.setEvaluator(new FloatEvaluator());
  8. animator2.start();
  1. ObjectAnimator animator1 = new ObjectAnimator();
  2. animator1.setTarget(textView1);
  3. animator1.setPropertyName("translationY");
  4. animator1.setFloatValues(0f, -100f); animator1.setDuration(1000);
  5. animator1.setInterpolator(new LinearInterpolator());
  6. animator1.setEvaluator(new AbstractFunction() {
  7. //自定义为TypeEvaluator
  8. @Override
  9. public float getValue(float input) {
  10. return input * 2 + 3;
  11. }
  12. });
  13. animator1.start();
  1. ObjectAnimator animator2 = new ObjectAnimator();
  2. animator2.setTarget(textView1);
  3. animator2.setPropertyName("translationY");
  4. animator2.setFloatValues(0f, -100f); animator1.setDuration(1000);
  5. animator2.setInterpolator(new AbstractFunction() {
  6. //自定义为TypeEvaluator
  7. @Override
  8. public float getValue(float input) {
  9. return input * 2 + 3;
  10. }
  11. });
  12. animator2.setEvaluator(new FloatEvaluator());
  13. animator2.start();

Canvas

Canvas的动画顺序是从后往前,是倒着来的!!很重要!!!!

Matrix

set pre post

Matrix调用一系列set,pre,post方法时,可视为将这些方法插入到一个队列.当然,按照队列中从头至尾的顺序调用执行.
其中pre表示在队头插入一个方法,post表示在队尾插入一个方法.而set表示把当前队列清空,并且总是位于队列的最中间位置.当执行了一次set后:pre方法总是插入到set前部的队列的最前面,post方法总是插入到set后部的队列的最后面

例一:

  1. Matrix m = new Matrix();
  2. m.setRotate(45);
  3. m.setTranslate(80, 80);

只有m.setTranslate(80, 80)有效,因为m.setRotate(45);被清除.

例子二:

  1. Matrix m = new Matrix();
  2. m.setTranslate(80, 80);
  3. m.postRotate(45);

先执行m.setTranslate(80, 80);后执行m.postRotate(45);

例子三:

  1. Matrix m = new Matrix();
  2. m.setTranslate(80, 80);
  3. m.preRotate(45);

先执行m.preRotate(45);后执行m.setTranslate(80, 80);

例子四:

  1. Matrix m = new Matrix();
  2. m.preScale(2f,2f);
  3. m.preTranslate(50f, 20f);
  4. m.postScale(0.2f, 0.5f);
  5. m.postTranslate(20f, 20f);

执行顺序:m.preTranslate(50f, 20f)-->m.preScale(2f,2f)-->m.postScale(0.2f, 0.5f)-->m.postTranslate(20f, 20f)
注意:m.preTranslate(50f, 20f)m.preScale(2f,2f)先执行,因为它查到了队列的最前端.

例子五:

  1. Matrix m = new Matrix();
  2. m.postTranslate(20, 20);
  3. m.preScale(0.2f, 0.5f);
  4. m.setScale(0.8f, 0.8f);
  5. m.postScale(3f, 3f);
  6. m.preTranslate(0.5f, 0.5f);

执行顺序:m.preTranslate(0.5f, 0.5f)-->m.setScale(0.8f, 0.8f)-->m.postScale(3f, 3f)
注意:m.setScale(0.8f, 0.8f)清除了前面的m.postTranslate(20, 20)m.preScale(0.2f, 0.5f);

Canvas.setMatrix(matrix) 和 Canvas.concat(matrix)

Camera

三维变换,我们来说下Camera的坐标系

图中黄色圆点就是虚拟相机的位置,其他标识都是x、y、z轴的旋转方向。

Camera.rotate*()

三维旋转,rotateX(deg)rotateY(deg)rotateZ(deg)rotate(x, y, z)

接下来说一个例子,原图如下:

  1. canvas.save();
  2. camera.rotateX(30); // 旋转 Camera 的三维空间
  3. camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
  4. canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
  5. canvas.restore();

另外,CameraCanvas 一样也需要保存和恢复状态才能正常绘制,不然在界面刷新之后绘制就会出现问题。所以完整的代码应该是这样的:

  1. canvas.save();
  2. camera.save(); // 保存 Camera 的状态
  3. camera.rotateX(30); // 旋转 Camera 的三维空间
  4. camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
  5. camera.restore(); // 恢复 Camera 的状态
  6. canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
  7. canvas.restore();

效果如下:

我们发现,旋转的结果不是堆成的。这是因为,Camera的旋转操作都是基于原点的,而且不能改变,所以我们需要借助canvastranslate()方法,如下:

  1. canvas.save();
  2. camera.save(); // 保存 Camera 的状态
  3. camera.rotateX(30); // 旋转 Camera 的三维空间
  4. canvas.translate(centerX, centerY); // 旋转之后把投影移动回来
  5. camera.applyToCanvas(canvas); // 把旋转投影到 Canvas
  6. canvas.translate(-centerX, -centerY); // 旋转之前把绘制内容移动到轴心(原点)
  7. camera.restore(); // 恢复 Camera 的状态
  8. canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
  9. canvas.restore();

Canvas 的几何变换顺序是反的,所以要把移动到中心的代码写在下面,把从中心移动回来的代码写在上面。

Camera.setLocation(x, y, z)

设置虚拟相机的位置
注意!这个方法有点奇葩,它的参数的单位不是像素,而是 inch,英寸。 英寸和像素的换算单位被写死为了 72 像素。在 Camera 中,相机的默认位置是 (0, 0, -8)(英寸)8 x 72 = 576,所以它的默认位置是 (0, 0, -576)(像素)

这个方法主要是用来解决这个问题:

如果绘制的内容过大,当它翻转起来的时候,就有可能出现图像投影过大的「糊脸」效果。而且由于换算单位被写死成了 72像素,而不是和设备 dpi 相关的,所以在像素越大的手机上,这种「糊脸」效果会越明显。

解决方法就是:使用 setLocation() 方法来把相机往后移动

参考

Matrix的set,pre,post调用顺序
Canvas 对绘制的辅助——范围裁切和几何变换。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注