zoukankan      html  css  js  c++  java
  • Android动画原理分析

    最近在Android上做了一些动画效果,网上查了一些资料,有各种各样的使用方式,于是乘热打铁,想具体分析一下动画是如何实现的,Animation, Animator都有哪些区别等等。

    首先说Animationandroid.view.animation.Animation)对象。

    无论是用纯java代码构建Animation对象,还是通过xml文件定义Animation,其实最终的结果都是

    Animation a = new AlphaAnimation();
    Animation b = new ScaleAnimation();
    Animation c = new RotateAnimation();
    Animation d = new TranslateAnimation();

    分别是透明度,缩放,旋转,位移四种动画效果。

    而我们使用的时候,一般是用这样的形式:

    View.startAnimation(a);

    那么就来看看View中的startAnimation()方法。

    1.View.startAnimation(Animation)

    先是调用View.setAnimation(Animation)方法给自己设置一个Animation对象,这个对象是View类中的一个名为mCurrentAnimation的成员变量。

    然后它调用invalidate()来重绘自己。

    我想,既然setAnimation()了,那么它要用的时候,肯定要getAnimation(),找到这个方法在哪里调用就好了。于是通过搜索,在View.draw(Canvas, ViewGroup, long)方法中发现了它的调用,代码片段如下:

    2.View.draw(Canvas, ViewGroup, long)

    其中调用了View.drawAnimation()方法。

    3.View.drawAnimation(ViewGroup, long, Animation, boolean)

    代码片段如下:

    其中调用了Animation.getTransformation()方法。

    4.Animation.getTransformation(long, Transformation, float)

    该方法直接调用了两个参数Animation.getTransformation()方法。

    5.Animation.getTransformation(long, Transformation)

    该方法先将参数currentTime处理成一个float表示当前动画进度,比如说,一个2000ms的动画,已经执行了1000ms了,那么进度就是0.5或者说50%。

    然后将进度值传入插值器(Interpolator)得到新的进度值,前者是均匀的,随着时间是一个直线的线性关系,而通过插值器计算后得到的是一个曲线的关系。

    然后将新的进度值和Transformation对象传入applyTranformation()方法中。

    6.Animation.applyTransformation(float, Transformation)

    Animation的applyTransformation()方法是空实现,具体实现它的是Animation的四个子类,而该方法正是真正的处理动画变化的过程。分别看下四个子类的applyTransformation()的实现。

    ScaleAnimation

    AlphaAnimation

    RotateAnimation

    TranslateAnimation

    可见applyTransformation()方法就是动画具体的实现,系统会以一个比较高的频率来调用这个方法,一般情况下60FPS,是一个非常流畅的画面了,也就是16ms,为了验证这一点,我在applyTransformation方法中加入计算时间间隔并打印的代码进行验证,代码如下:

    最终得到的log如下图所示:

    右侧是“手动”计算出来的时间差,有一定的波动,但大致上是16-17ms的样子,左侧是日志打印的时间,时间非常规则的相差20ms。

    于是,根据以上的结果,可以得出以下内容:

    1.首先证明了一点,Animation.applyTransformation()方法,是动画具体的调用方法,我们可以覆写这个方法,快速的制作自己的动画。

    2.另一点,为什么是16ms左右调用这个方法呢?是谁来控制这个频率的?

    对于以上的疑问,我有两个猜测:

    1.系统自己以postDelayed(this, 16)的形式调用的这个方法。具体的写法,请参考《使用线程实现视图平滑滚动》

    2.系统一个死循环疯狂的调用,运行一系列方法走到这个位置的间隔刚好是16ms左右,如果主线程卡了,这个间隔就变长了。

    为了找到答案,我在Stack Overflow上发帖问了下,然后得到一个情报,那就是让我去看看Choreographer(android.view.Choreographer)类。

    1.Choreographer的构造方法

    看了下Choreographer类的构造方法,是private的,不允许new外部类new,于是又发现了它有一个静态的getInstance()方法,那么,我需要找到getInstance()方法被谁调用了,就可以知道Choreographer对象在什么地方被使用。一查,发现Choreographer.getInstance()在ViewRootImpl的构造方法中被调用。以下代码是ViewRootImpl.ViewRootImpl(Context, Display)的片段。

    OK,找到了ViewRootImpl中拥有一个mChoreographer对象,接下来,我需要去找,它如何被使用了,调用了它的哪些方法。于是发现如下代码:

    scheduleTraversals()方法中,发现了这个对象的使用。

    2.Choreographer.postCallback(int, Runnable, Object)

    该方法辗转调用了两个内部方法,最终是调用了Choreographer.postCallbackDelayedInternal()方法。

    3.Choreographer.postCallbackDelayedInternal(int, Object, Object, long)

    这个方法中,

    1.首先拿到当前的时间。

    这里参数中有一个delay,它的值可以具体查看一下,你会发现它就是一个静态常量,定义在Choreographer类中,它的值是10ms。也就是说,理想情况下,所有的时间都是以100FPS来运行的。

    2.将要执行的内容加入到一个mCallbackQueues中。

    3.然后执行scheduleFrameLocked()或者发送一个Message。

    接着我们看Choreographer.scheduleFrameLocked(long)方法

    4.Choreographer.scheduleFrameLocked(long)

    if判断进去的部分是是否使用垂直同步,暂时不考虑。

    else进去的部分,还是将消息发送到mHandler对象中。那我们就直接来看mHandler对象就好了

    5.Choreographer.FrameHandler

    mHandler实例的类型是FrameHandler,它的定义就在Choreographer类中,代码如下:

    它的处理方法中有三个分支,但最终都会调用这个doFrame()方法。

    6.Choreographer.doFrame()

    doFrame()方法巴拉巴拉一大段,但在下面有非常工整的一段代码,一下就吸引了我的眼球。

    它调用了三次doCallbacks()方法,暂且不说这个方法是干什么的,但从它的第一个参数可以看到分别是输入(INPUT),动画(ANIMATION),遍历(TRAVERSAL)

    于是,我先是看了下这三个常量的意义。下图所示:

    显然,注释是说:输入事件最先处理,然后处理动画,最后才处理view的布局和绘制。接下来我们看看Choreographer.doCallbacks()里面做了什么。

    7.Choreographer.doCallbacks(int, long)

    这个方法的操作非常统一,有三种不同类型的操作(输入,动画,遍历),但在这里却看不见这些具体事件的痕迹,这里我们不得不分析一下mCallbackQueues这个成员变量了。

    mCallbackQueues是一个CallbackQueue对象数组。而它的下标,其意义并不是指元素1,元素2,元素3……而是指类型,请看上面doCallbacks()的代码,参数callbackType传给了mCallbackQueues[callbackType]中,而callbackType是什么呢?

    其实就是前面说到的三个常量,CALLBACK_INPUT, CALLBACK_ANIMATION, CALLBACK_TRAVERVAL

    那么只需要根据不同的callbackType,就可以从这个数组里面取出不同类型的CallbackQueue对象来。

    那么CallbackQueue又是什么呢?

    CallbackQueueChoreographer的一个内部类,其中我认为有两个很重要的方法,分别是:extractDueCallbacksLocked(long)addCallbackLocked(long, Object, Object)

    先说addCallbackLocked(long, Object, Object)

    1.CallbackQueue.addCallbackLocked(long, Object, Object)

    首先它通过一个内部方法构建了一个CallbackRecord对象,然后后面的if判断和while循环,大致上是将参数中的对象链接在CallbackRecord的尾部。其实CallbackRecord就是一个链表结构的对象。

    2.CallbackQueue.extractDueCallbacksLocked(long)

    这个方法是根据当前的时间,选出执行链表中与该时间最近的一个操作来处理,实际上,我们可以通俗的理解为“跳帧”。

    想象一下,如果主线程运行的非常快速,非常流畅,每一步都能在10ms内准时运行到,那么我们的执行链表中的元素始终只有一个。

    如果主线程中做了耗时操作,那么各种事件一直在往各自的链表中添加,但是当主线程有空来执行的时候,发现链表已经那么多积累的过期的事件了,那么就直接选择最后一个来执行,那么界面上看起来,就是卡顿了一下。

    到这里为止,我们得出以下结论:

    1.控制外部输入事件处理,动画执行,UI变化都是在同一个类中做的处理,即是Choreographer,其中它规定的了理想的运行间隔为10ms,因为各种操作需要花费一定的时间,所以外部执行的间隔统计出来是大约16ms。

    2.在Choreographer对象中有三条链表,分别保存着待处理的输入事件,待处理的动画事件,待处理的遍历事件。

    3.每次执行的时候,Choreographer会根据当前的时间,只处理事件链表中最后一个事件,当有耗时操作在主线程时,事件不能及时执行,就会出现所谓的“跳帧”,“卡顿”现象。

    4.Choreographer的共有方法postCallback(callbackType, Object)是往事件链表中放事件的方法。而doFrame()是消耗这些事件的方法。

    事到如今,已经探究出不少有用的细节。这里,又给自己提出一个问题,根据以上的事实,那么,只需要找到哪些东西再往这三条链表中放事件呢?

    于是进一步探究一下。我们只需要找到postCallback()被哪些方法调用了即可。

    于是请点击这里,通过grepcode列举了调用postCallback()的方法。

    搞明白了Choreographer的工作原理,再去看ObjectAnimator,ValueAnimator的实现,就非常的轻松了。

    ObjectAnimator.start()方法实际上是辗转几次调用了ValueAnimator的start()方法,ValueAnimator.start()又调用了一个临时变量animationHandler.start()。

    animationHandler实际上是一个Runnable,其中start()方法调用了scheduleAnimation()。

    而这个方法:

    调用了postCallback()方法。

    将this(Runnable)post之后,实际上肯定就是要执行Runnable.run()方法

    run()方法中又调用了doAnimationFrame()方法。这个方法具体的实现了动画的某一帧的过程,然后再次调用了scheduleAnimation()方法。

    就相当于postDelayed(this, 16)这种方式了。

    到这里为止,对Animation原理的分析就到此结束了,本来只想分析下Animation的实现过程,没想到顺带研究了一下Choreographer的工作原理,今天收获还是不少。

    其实还有好多疑问,技术学习也一天急不得,靠的是每日慢慢的积累,相信总有一天,各种疑惑都会迎刃而解。

  • 相关阅读:
    前端学习笔记之BOM和DOM
    JAVA学习笔记之图解JAVA参数传递
    Python学习笔记之函数参数传递 传值还是传引用
    Java学习笔记之对象的复制和克隆
    如何科学正确的使用搜索引擎
    JAVA学习笔记之JAVA 对象引用以及赋值
    前端学习笔记之Z-index详解
    Python面试题目之Python的复制和赋值浅析
    Python面试题目之(针对dict或者set数据类型)边遍历 边修改 报错dictionary changed size during iteration
    判断对象是否为数组/函数
  • 原文地址:https://www.cnblogs.com/kross/p/4087780.html
Copyright © 2011-2022 走看看