0%

Android 动画原理

View Animation原理

  1. 首先,当调用了 View.startAnimation() 时动画并没有马上就执行,而是通过 invalidate() 层层通知到 ViewRootImpl 发起一次遍历 View 树的请求,而这次请求会等到接收到最近一帧到了的信号时才去发起遍历 View 树绘制操作。
  2. 从 DecorView 开始遍历,绘制流程在遍历时会调用到 View 的 draw() 方法,当该方法被调用时,如果 View 有绑定动画,那么会去调用applyLegacyAnimation(),这个方法是专门用来处理动画相关逻辑的。
  3. 在 applyLegacyAnimation() 这个方法里,如果动画还没有执行过初始化,先调用动画的初始化方法 initialized(),同时调用 onAnimationStart() 通知动画开始了,然后调用 getTransformation() 来根据当前时间计算动画进度,紧接着调用 applyTransformation() 并传入动画进度来应用动画。
  4. getTransformation() 这个方法有返回值,如果动画还没结束会返回 true,动画已经结束或者被取消了返回 false。所以 applyLegacyAnimation() 会根据 getTransformation() 的返回值来决定是否通知 ViewRootImpl 再发起一次遍历请求,返回值是 true 表示动画没结束,那么就去通知 ViewRootImpl 再次发起一次遍历请求。然后当下一帧到来时,再从 DecorView 开始遍历 View 树绘制,重复上面的步骤,这样直到动画结束。
  5. 有一点需要注意,动画是在每一帧的绘制流程里被执行,所以动画并不是单独执行的,也就是说,如果这一帧里有一些 View 需要重绘,那么这些工作同样是在这一帧里的这次遍历 View 树的过程中完成的。每一帧只会发起一次 perfromTraversals() 操作。

View Animation 是在绘制的时候,改变 view 的视觉效果来实现动画的。所以不会对 view 的测量和布局过程有影响。

View 的动画是通过触发绘制过程来执行 draw 的。因为动画是连续的,所以需要不停的触发。

View Animation为什么移动位置后,点击事件的响应依旧是在原来位置上?

因为动画是在 draw 时候形成的,也就是说只是视觉效果。其并没有改变它本身在父类中的位置;

View Animation中如果对 View 做放大缩小得动画,那么其宽度高度值是否会变化?

View Animation 是在绘制的时候,改变 view 的视觉效果来实现动画的。所以不会对 view 的测量和布局过程有影响。

属性动画的原理

  1. ValueAnimator 属性动画调用了 start() 之后,会先去进行一些初始化工作,包括变量的初始化、通知动画开始事件;
  2. 然后通过 AnimationHandler 将其自身 this 添加到 mAnimationCallbacks 队列里,AnimationHandller 是一个单例类,为所有的属性动画服务,列表里存放着所有正在进行或准备开始的属性动画;
  3. 如果当前存在要运行的动画,那么 AnimationHandler 会去通过 Choreographer 向底层注册监听下一个屏幕刷新信号,当接收到信号时,它的 mFrameCallback 会开始进行工作,工作的内容包括遍历列表来分别处理每个属性动画在当前帧的行为,处理完列表中的所有动画后,如果列表还不为 0,那么它又会通过 Choreographer 再去向底层注册监听下一个屏幕刷新信号事件,如此反复,直至所有的动画都结束。
  4. AnimationHandler 遍历列表处理动画是在 doAnimationFrame() 中进行,而具体每个动画的处理逻辑则是在各自,也就是 ValueAnimator 的 doAnimationFrame() 中进行,各个动画如果处理完自身的工作后发现动画已经结束了,那么会将其在列表中的引用赋值为空,AnimationHandler 最后会去将列表中所有为 null 的都移除掉,来清理资源。
  5. 每个动画 ValueAnimator 在处理自身的动画行为时,首先,如果当前是动画的第一帧,那么会根据是否有”跳过片头”(setCurrentPlayTime())来记录当前动画第一帧的时间 mStartTime 应该是什么。
  6. 第一帧的动画其实也就是记录 mStartTime 的时间以及一些变量的初始化而已,动画进度仍然是 0,所以下一帧才是动画开始的关键,但由于属性动画的处理工作是在绘制界面之前的,那么有可能因为绘制耗时,而导致 mStartTime 记录的第一帧时间与第二帧之间隔得太久,造成丢了开头的多帧,所以如果是这种情况下,会进行 mStartTime 的修正。
  7. 修正的具体做法则是当绘制工作完成后,此时,再根据当前时间与 mStartTime 记录的时间做比较,然后进行修正。
  8. 如果是在动画过程中的某一帧才出现绘制耗时现象,那么,只能表示无能为力了,丢帧是避免不了的了,想要解决就得自己去分析下为什么绘制会耗时;而如果是在第一帧是出现绘制耗时,那么,系统还是可以帮忙补救一下,修正下 mStartTime 来达到避免丢帧。

  1. 当接收到屏幕刷新信号后,AnimationHandler 会去遍历列表,将所有待执行的属性动画都取出来去计算当前帧的动画行为。
  2. 每个动画在处理当前帧的动画逻辑时,首先会先根据当前时间和动画第一帧时间以及动画的持续时长来初步计算出当前帧时动画所处的进度,然后会将这个进度值等价转换到 0-1 区间之内。
  3. 接着,插值器会将这个经过初步计算之后的进度值根据设定的规则计算出实际的动画进度值,取值也是在 0-1 区间内。
  4. 计算出当前帧动画的实际进度之后,会将这个进度值交给关键帧机制,来换算出我们需要的值,比如 ValueAnimator.ofInt(0, 100) 表示我们需要的值变化范围是从 0-100,那么插值器计算出的进度值是 0-1 之间的,接下去就需要借助关键帧机制来映射到 0-100 之间。
  5. 关键帧的数量是由 ValueAnimator.ofInt(0, 1, 2, 3) 参数的数量来决定的,比如这个就有四个关键帧,第一帧和最后一帧是必须的,所以最少会有两个关键帧,如果参数只有一个,那么第一帧默认为 0,最后一帧就是参数的值。当调用了这个 ofInt() 方法时,关键帧组也就被创建了。
  6. 当只有两个关键帧时,映射的规则是,如果没有设置估值器,那么就等比例映射,比如动画进度为 0.5,需要的值变化区间是 0-100,那么等比例映射后的值就是 50,那么我们在 onAnimationUpdate 的回调中通过 animation.getAnimatedValue() 获取到的值 50 就是这么来的。
  7. 如果有设置估值器,那么就按估值器的规则来进行映射。
  8. 当关键帧超过两个时,需要先找到当前动画进度是落于哪两个关键帧之间,然后将这个进度值先映射到这两个关键帧之间的取值,接着就可以将这两个关键帧看成是第一帧和最后一帧,那么就可以按照只有两个关键帧的情况下的映射规则来进行计算了。
  9. 而进度值映射到两个关键帧之间的取值,这就需要知道每个关键帧在整个关键帧组中的位置信息,或者说权重。而这个位置信息是在创建每个关键帧时就传进来的。onInt() 的规则是所有关键帧按等比例来分配权重,比如有三个关键帧,第一帧是 0,那么第二帧就是 0.5, 最后一帧 1。