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的工作原理,今天收获还是不少。

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

  • 相关阅读:
    《C# to IL》第一章 IL入门
    multiple users to one ec2 instance setup
    Route53 health check与 Cloudwatch alarm 没法绑定
    rsync aws ec2 pem
    通过jvm 查看死锁
    wait, notify 使用清晰讲解
    for aws associate exam
    docker 容器不能联网
    本地运行aws lambda credential 配置 (missing credential config error)
    Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?
  • 原文地址:https://www.cnblogs.com/kross/p/4087780.html
Copyright © 2011-2022 走看看