美文网首页
View 动画 Animation 运行原理解析

View 动画 Animation 运行原理解析

作者: 有腹肌的豌豆Z | 来源:发表于2020-10-20 15:43 被阅读0次

    一、简介

    Animation 动画的扩展性很高,系统只是简单的为我们封装了几个基本的动画:平移、旋转、透明度、缩放等等,感兴趣的可以去看看这几个动画的源码,它们都是继承自 Animation 类,然后实现了 applyTransformation() 方法,在这个方法里通过 Transformation 和 Matrix 实现各种各样炫酷的动画,所以,如果想要做出炫酷的动画效果,这些还是需要去搞懂的。

    首先看看 Animation 动画的基本用法:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            TextView tv=findViewById(R.id.tv);
            // 创建一个 animation
            ScaleAnimation scaleAnimation = new ScaleAnimation(0,1,0,1);
            // 各种参数配置
            scaleAnimation.setDuration(3000);
            scaleAnimation.setFillAfter(true);
            // 启动动画
            tv.startAnimation(scaleAnimation);
        }
    }
    

    二、源码分析

    View.startAnimation()

    刚开始接触源码分析可能不清楚该从哪入手,建议可以从我们使用它的地方来 startAnimation():

        /**
         * Start the specified animation now.
         *
         * @param animation the animation to start now
         */
        public void startAnimation(Animation animation) {
            animation.setStartTime(Animation.START_ON_FIRST_FRAME);
            setAnimation(animation);
            invalidateParentCaches();
            invalidate(true);
        }
    

    代码不多,调用了四个方法,那么一个个跟进去看看,先是 setStartTime() :
    设置初始值

        public void setStartTime(long startTimeMillis) {
            mStartTime = startTimeMillis;
            mStarted = mEnded = false;
            mCycleFlip = false;
            mRepeated = 0;
            mMore = true;
        }
    

    继续看看 setAnimation():

        public void setAnimation(Animation animation) {
            mCurrentAnimation = animation;
    
            if (animation != null) {
                // If the screen is off assume the animation start time is now instead of
                // the next frame we draw. Keeping the START_ON_FIRST_FRAME start time
                // would cause the animation to start when the screen turns back on
                if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
                        && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
                    animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
                }
                animation.reset();
            }
        }
    

    View 里面有一个 Animation 类型的成员变量,所以这个方法其实是将我们 new 的 ScaleAnimation 动画跟 View 绑定起来而已,也没有运行动画的逻辑,继续往下看看 invalidateParentCached():

        @UnsupportedAppUsage
        protected void invalidateParentCaches() {
            if (mParent instanceof View) {
                ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
            }
        }
    

    invalidateParentCaches() 这方法更简单,给 mPrivateFlags 添加了一个标志位,虽然还不清楚干嘛的,但可以先留个心眼,因为 mPrivateFlags 这个变量在阅读跟 View 相关的源码时经常碰到,那么可以的话能搞明白就搞明白,但目前跟我们想要找出动画到底什么时候开始执行的关系好像不大,先略过,继续跟进 invalidate():

        @UnsupportedAppUsage
        public void invalidate(boolean invalidateCache) {
            invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
        }
    
        void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                boolean fullInvalidate) {
            if (mGhostView != null) {
                mGhostView.invalidate(true);
                return;
            }
    
            if (skipInvalidate()) {
                return;
            }
    
            // Reset content capture caches
            mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK;
            mContentCaptureSessionCached = false;
    
            if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                    || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                    || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                    || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
                if (fullInvalidate) {
                    mLastIsOpaque = isOpaque();
                    mPrivateFlags &= ~PFLAG_DRAWN;
                }
    
                mPrivateFlags |= PFLAG_DIRTY;
    
                if (invalidateCache) {
                    mPrivateFlags |= PFLAG_INVALIDATED;
                    mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
                }
    
                // Propagate the damage rectangle to the parent view.
                final AttachInfo ai = mAttachInfo;
                final ViewParent p = mParent;
                if (p != null && ai != null && l < r && t < b) {
                    final Rect damage = ai.mTmpInvalRect;
                    damage.set(l, t, r, b);
                    p.invalidateChild(this, damage);
                }
    
                // Damage the entire projection receiver, if necessary.
                if (mBackground != null && mBackground.isProjected()) {
                    final View receiver = getProjectionReceiver();
                    if (receiver != null) {
                        receiver.damageInParent();
                    }
                }
            }
        }
    

    mParent 一般都是ViewGroup ,其实是调用了ViewGroup的invalidateChild
    所以 invalidate() 内部其实是调用了 ViewGroup 的 invalidateChild(),再跟进看看:

    public final void invalidateChild(View child, final Rect dirty) {
            ViewParent parent = this;
            if (attachInfo != null) {
               ....
                do {
                   ....
                    第一次循环的时候parent是ViewGroup本身,循环终止条件是parent==null,
    所以可以猜测这个方法会返回当前ViewGriuo的parent,跟进确认一下
                    parent = parent.invalidateChildInParent(location, dirty);
                    ....
                } while (parent != null);
            }
    }
    

    这里有一个 do{}while() 的循环操作,第一次循环的时候 parent 是 this,即 ViewGroup 本身,所以接下去就是调用 ViewGroup 本身的 invalidateChildInParent() 方法,然后循环终止条件是 patent == null,所以可以猜测这个方法返回的应该是 ViewGroup 的 parent,跟进看看:

    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
            if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
                ....
                return mParent;
            }
            return null;
    }
    

    所以关键是 PFLAG_DRAWN 和 PFLAG_DRAWING_CACHE_VALID 这两个是什么时候赋值给 mPrivateFlags,因为只要有两个标志中的一个时,该方法就会返回 mParent,具体赋值的地方还不大清楚,但能确定的是动画执行时,它是满足 if 条件的,也就是这个方法会返回 mParent。

    一个具体的 View 的 mParent 是 ViewGroup,ViewGroup 的 mParent 也是 ViewGoup,所以在 do{}while() 循环里会一直不断的寻找 mParent,而一颗 View 树最顶端的 mParent 是 ViewRootImpl,所以最终是会走到了 ViewRootImpl 的 invalidateChildInParent() 里去了。

    至于一个界面的 View 树最顶端为什么是 ViewRootImpl,这个就跟 Activity 启动过程有关了。我们都清楚,在 onCreate 里 setContentView() 的时候,是将我们自己写的布局文件添加到以 DecorView 为根布局的一个 ViewGroup 里,也就是说 DevorView 才是 View 树的根布局,那为什么又说 View 树最顶端其实是 ViewRootImpl 呢?

    这是因为在 onResume() 执行完后,WindowManager 将会执行 addView(),然后在这里面会去创建一个 ViewRootImpl 对象,接着将 DecorView 跟 ViewRootImpl 对象绑定起来,并且将 DecorView 的 mParent 设置成 ViewRootImpl,而 ViewRootImpl 是实现了 ViewParent 接口的,所以虽然 ViewRootImpl 没有继承 View 或 ViewGroup,但它确实是 DecorView 的 parent。这部分内容应该属于 Activity 的启动过程相关原理的,所以本篇只给出结论,不深入分析了,感兴趣的可以自行搜索一下。

    .....

    动画真正执行的地方

    那么,到这里,我们可以猜测,动画其实真正执行的地方应该是在 ViewRootImpl 发起的遍历 View 树的这个过程中。测量、布局、绘制,View 显示到屏幕上的三个基本操作都是由 ViewRootImpl 的 performTraversals() 来控制,而作为 View 树最顶端的 parent,要控制这颗 Veiw 树的三个基本操作,只能通过层层遍历。所以,测量、布局、绘制三个基本操作的执行都会是一次遍历操作。

    我在跟着这三个流程走的时候,最后发现,在跟着绘制流程走的时候,看到了跟动画相关的代码,所以我们就跳过其他两个流程,直接看绘制流程:

    这张图不是我画的,在网上找的,绘制流程的开始是由 ViewRootImpl 发起的,然后从 DecorView 开始遍历 View 树。而遍历的实现,是在 View#draw() 方法里的。我们可以看看这个方法的注释:

    这个方法里主要做了上述六件事,大体上就是如果当前 View 需要绘制,就会去调用自己的 onDraw(),然后如果有子 View,就会调用dispatchDraw() 将绘制事件通知给子 View。ViewGroup 重写了 dispatchDraw(),调用了 drawChild(),而 drawChild() 调用了子 View 的 draw(Canvas, ViewGroup, long),而这个方法又会去调用到 draw(Canvas) 方法,所以这样就达到了遍历的效果。整个流程就像上上图中画的那样。

    在这个流程中,当跟到 draw(Canvas, ViewGroup, long) 里时,发现了跟动画相关的代码:


    还记得我们调用 View.startAnimation(Animation) 时将传进来的 Animation 赋值给 mCurrentAnimation 了么。

        public Animation getAnimation() {
            return mCurrentAnimation;
        }
    

    所以当时传进来的 Animation ,现在拿出来用了,那么动画真正执行的地方应该也就是在 applyLegacyAnimation() 方法里了(该方法在 android-22 版本及之前的命名是 drawAnimation)

    这下确定动画真正开始执行是在什么地方了吧,都看到 onAnimationStart() 了,也看到了对动画进行初始化,以及调用了 Animation 的 getTransformation,这个方法是动画的核心,再跟进去看看:

    这个方法里做了几件事:

    记录动画第一帧的时间
    根据当前时间到动画第一帧的时间这之间的时长和动画应持续的时长来计算动画的进度
    把动画进度控制在 0-1 之间,超过 1 的表示动画已经结束,重新赋值为 1 即可
    根据插值器来计算动画的实际进度
    调用 applyTransformation() 应用动画效果
    

    所以,到这里我们已经能确定 applyTransformation() 是什么时候回调的,动画是什么时候才真正开始执行的。那么 Q1 总算是搞定了,Q2 也基本能理清了。因为我们清楚, applyTransformation() 最终是在绘制流程中的 draw() 过程中执行到的,那么显然在每一帧的屏幕刷新信号来的时候,遍历 View 树是为了重新计算屏幕数据,也就是所谓的 View 的刷新,而动画只是在这个过程中顺便执行的。

    接下去就是 Q3 了,我们知道 applyTransformation() 是动画生效的地方,这个方法不断的被回调时,参数会传进来动画的进度,所以呈现效果就是动画根据进度在运行中。

    但是,我们从头分析下来,找到了动画真正执行的地方,找到了 applyTransformation() 被调用的地方,但这些地方都没有看到任何一个 for 或者 while 循环啊,也就是一次 View 树的遍历绘制操作,动画也就只会执行一次而已啊?那么它是怎么被回调那么多次的?

    我们知道 applyTransformation() 是在 getTransformation() 里被调用的,而这个方法是有一个 boolean 返回值的,我们看看它的返回逻辑是什么:


    也就是说 getTransformation() 的返回值代表的是动画是否完成,还记得是哪里调用的 getTransformation() 吧,去 applyLegacyAnimation() 里看看取到这个返回值后又做了什么:

    当动画如果还没执行完,就会再调用 invalidate() 方法,层层通知到 ViewRootImpl 再次发起一次遍历请求,当下一帧屏幕刷新信号来的时候,再通过 performTraversals() 遍历 View 树绘制时,该 View 的 draw 收到通知被调用时,会再次去调用 applyLegacyAnimation() 方法去执行动画相关操作,包括调用 getTransformation() 计算动画进度,调用 applyTransformation() 应用动画。

    也就是说,动画很流畅的情况下,其实是每隔 16.6ms 即每一帧到来的时候,执行一次 applyTransformation(),直到动画完成。所以这个 applyTransformation() 被回调多次是这么来的,而且这个回调次数并没有办法人为进行设定。

    这就是为什么当动画持续时长越长时,这个方法打出的日志越多次的原因。

    还记得 getTransformation() 方法在计算动画进度时是根据参数传进来的 currentTime 的么,而这个 currentTime 可以理解成是发起遍历操作这个时刻的系统时间(实际 currentTime 是在 Choreographer 的 doFrame() 里经过校验调整之后的一个时间,但离发起遍历操作这个时刻的系统时间相差很小,所以不深究的话,可以像上面那样理解,比较容易明白)。

    小结

    综上,我们稍微整理一下:

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

    牛🐮🐮牛

    相关文章

      网友评论

          本文标题:View 动画 Animation 运行原理解析

          本文链接:https://www.haomeiwen.com/subject/nordmktx.html