Android动画篇(七)—— 补间动画(Tween Animation)的运行原理

前言:不要在努力拼搏的年纪去选择安逸享乐,为了不让生活留下遗憾和后悔,我们要抓住一切改变生活的机会,生命不止,奋斗不息。

一、概述

在前面的文章中我们详细讲解了补间动画(Tween Animation)的使用,系统为我们封装了几个基本的动画,也就是ScaleAnimation(缩放)、AlphaAnimation(透明)、RotateAnimation(旋转)、TranslateAnimation(平移),它们都是继承了Animation类,然后实现了applyTransformation()方法,然后通过Transformation转换类和Matrix矩形类实现了各种各样的动画效果。

那么补间动画执行的原理是怎么样的呢?我们从源码的角度查看这个问题,查看源码我们带着问题去看去查找,避免大海捞针被转晕。我们先来看看动画的使用,这里以RotateAnimation(旋转)为例:

//创建RotateAnimation实例
RotateAnimation rotateAnimation = new RotateAnimation(0f, -650f,
                        Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
//设置动画时间2000毫秒
rotateAnimation.setDuration(2000);
//设置保持动画结束时的状态
rotateAnimation.setFillAfter(true);
//控件绑定动画
mTextView.startAnimation(rotateAnimation);

效果如下:

这里围绕自身旋转逆时针方向650度,从代码看到是通过mTextView.startAnimation(rotateAnimation)将动画实例通过参数传给控件,然后控件就可以动画了。

二、动画执行源码分析

1、View树遍历(动画执行前)

我们顺藤摸瓜点击mTextView.startAnimation(rotateAnimation)方法进入源码,

    //View.java
    public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
    }

这里有四个方法,我们一个个来看,首先是通过setStartTime()设置了动画的开始时间,

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

这里只是对一些变量进行赋值,再来看看下一个方法,设置动画setAnimation(animation),点击进去看源码:

    //View.java
    public void setAnimation(Animation animation) {
        mCurrentAnimation = animation;
        if (animation != null) {
                  if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
                    && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
                animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
            }
            animation.reset();
        }
    }

这里面也是将动画实例赋值给当前的成员变量,还没有动画的逻辑,我们回到上一层startAnimation()方法里查看下一个方法invalidateParentCaches(),

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

可以看到这里仅仅是设置动画标记,在视图构建或者属性改变时是必要的,再回到startAnimation()方法里面invalidate(true)

    //View.java
    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;
        }
          .................
            // 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);
            }
        }
     }

这里代码太多我就贴出重要的一部分,这里着重看p.invalidateChild(this, damage)

    //ViewGroup.java
    @Deprecated
    @Override
    public final void invalidateChild(View child, final Rect dirty) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null && attachInfo.mHardwareAccelerated) {
            // HW accelerated fast path
            onDescendantInvalidated(child, child);
            return;
        }

        ViewParent parent = this;
          .........
            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }
           .........
                parent = parent.invalidateChildInParent(location, dirty);
               
            } while (parent != null);
        }
    }

因为ViewParent p = mParent,this是View的子类ViewGroup,所以p.invalidateChild(this, damage)里面其实是调用了ViewGroup的invalidateChild(),这里有一个do{}while()循环,第一次的时候parent = this即ViewGroup,然后调用parent.invalidateChildInParent(location, dirty)方法,当parent == null的时候结束循环。

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {}方法中,只要条件成立就会返回mParent

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

        return null;
    }

((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0)是保持成立的,所以会一直返回mParent,那么说明View的mParent是ViewGroup,ViewGroup的mParent也是ViewGroup,而do{}while()循环一直找mParent,而一颗View最顶端的mParent是ViewRootImpl,所以最后走到ViewRootImpl的invalidateChildInParent()里面。

我们知道在onCreate()方法里面通过setContentView()将布局添加到以DecorView为根布局的一个ViewGroup里面,因为在onResume()执行完成后,WindowManager会执行addView()方法,然后会创建一个ViewRootImpl对象,与DecorView绑定起来,DecorView的mParent设置成ViewRootImpl,ViewRootImpl实现了ViewParent接口,所以ViewRootImpl虽然没有继承View或者ViewGroup,但是DecorView的mParent。我们找到ViewRootImpl的invalidateChildInParent()方法中,

    //ViewRootImpl.java
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);

        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }
         .......
        invalidateRectOnScreen(dirty);

        return null;
    }

从下面的源码得知,这里所有的返回值都变为null了,之前执行的do{}while()循坏也会停止,我们点击invalidateRectOnScreen(dirty)方法进去看看,接着进入 scheduleTraversals()方法中,

    //ViewRootImpl.java
    private void invalidateRectOnScreen(Rect dirty) {
        ......
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
    }
  //ViewRootImpl.java
  void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

主要看mTraversalRunnable,我们找到mTraversalRunnable这个类,

  //ViewRootImpl.java
  final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

我们打开doTraversal()方法,

  //ViewRootImpl.java
  void doTraversal() {
           .......
            performTraversals();
           .......
        }
    }

所以说,scheduleTraversals()是将 performTraversals()放到一个Runnable里面,在Choreographer的带执行对列里面,这些待执行的Runable会在最近的一个16.6ms屏幕刷新信号到来的时候执行,而performTraversals()是View的三大操作:测量、布局、绘制的发起者。

在Android屏幕刷新机制里,View树里面不管哪个View发起的绘制请求或者布局请求都会走到ViewRootImpl的scheduleTraversals()里面,然后在最新的一个屏幕刷新信号来到时,再通过ViewRootImpl的performTraversals()从根布局DecorView依次遍历View树去执行测量、布局、绘制三大操作,这也是为什么要求布局层次不能太深,因为每一次的刷新都会走到ViewRootImpl里面,然后在层层遍历到发生改变的VIew里去执行相应的布局和绘制操作。

所以在mTextView.startAnimation(rotateAnimation)源码分析的这个过程中,执行动画会内部会调用View的重绘操作invalidate(),最终走到ViewRootImpl的scheduleTraversals(),在下一个屏幕刷新信号到来的时候遍历View树刷新屏幕。得出结论:

在调用View.startAnimation(rotateAnimation)后,并没有立即执行动画,而是做了一下变量初始化操作,将View和Animation绑定起来,调用重绘操作,内部层层寻找mPartent,最终在ViewRootImpl的scheduleTraversals()发起一个遍历View树的请求,在最近一个屏幕信号刷新到来时执行这个请求,调用performTraversals()从根布局去遍历View树。

2、动画的执行

那么真正动画的执行是在ViewRootImpl发起的遍历View树的过程中,View显示在屏幕的过程中的测量、布局、绘制都是有ViewRootImpl的performTraversals()控制,这个View树的三个操作,只能通过层层遍历,所以基本操作的执行都会是一次遍历股过程。

 //View.java
 public void draw(Canvas canvas) {
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */
}

View遍历的实现是在View#Draw()方法里面执行的,这个方法大体上就做了六件事,当前View需要绘制,就会去调用自己的onDraw()方法,如果有子View就会去调用dispatchDraw()将绘制事件通知子View,ViewGroup重写了dispatchDraw(),调用了drawChild(),然后drawChild()调用子View的draw(Canvas, ViewGroup, long),而这个方法又会调用到draw(Canvas)方法,所以达到了遍历的效果。在测量和布局的方法中都没有动画的相关代码,但是在draw(Canvas, ViewGroup, long)发现了动画的相关代码:

    //View.java
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
        ......
        boolean more = false;
        ......
        Transformation transformToApply = null;
        ......
        final Animation a = getAnimation();
        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
      ......
     return more;
    }

这里的getAnimation()其实就是上面View.startAnimation(Animation)时传进来的成员变量,

    //View.java
    public Animation getAnimation() {
        return mCurrentAnimation;
    }

我们点进去applyLegacyAnimation()方法中,

    //View.java
    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }

        final Transformation t = parent.getChildTransformation();
        boolean more = a.getTransformation(drawingTime, t, 1f);
        if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
            if (parent.mInvalidationTransformation == null) {
                parent.mInvalidationTransformation = new Transformation();
            }
            invalidationTransform = parent.mInvalidationTransformation;
            a.getTransformation(drawingTime, invalidationTransform, 1f);
        } else {
            invalidationTransform = t;
        }

       .....
        return more;
    }

从上面看到a.initialize()onAnimationStart()有动画的初始化和开始,我们跟着getTransformation()方法看到动画的核心就在这里,

View.java

这个方法主要做了记录动画第一帧的时间,计算动画进度值,控制动画进度在0-1之间,根据插值器计算动画的实际进度(可以参考《Android动画篇(四)—— 属性动画ValueAnimator的高级进阶》),最后才在applyTransformation()执行动画的具体逻辑,在子类的具体实现相应的动画的逻辑。我们来看看ScaleAnimation的applyTransformation()具体做了什么。

ScaleAnimation.java

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float sx = 1.0f;
        float sy = 1.0f;
        float scale = getScaleFactor();
        if (mFromX != 1.0f || mToX != 1.0f) {
            sx = mFromX + ((mToX - mFromX) * interpolatedTime);
        }
        if (mFromY != 1.0f || mToY != 1.0f) {
            sy = mFromY + ((mToY - mFromY) * interpolatedTime);
        }
        if (mPivotX == 0 && mPivotY == 0) {
            t.getMatrix().setScale(sx, sy);
        } else {
            t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
        }
    }

applyTransformation()根据传入的比例参数计算需要操作的动画参数,然后由Transformation 做动画操作。其他的子类AlphaAnimation、RotateAnimation、TranslateAnimation都是会重写这个方法并实现具体的动画逻辑,这里就不一一举例了。

但是问题来了,applyTransformation()是动画不断回调的地方,根据传入的参数实现动画在运行中,而applyTransformation()被调用的地方没有循环,那么一次View树的遍历绘制也就执行一次动画?它是怎么被多次回调的?

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

View.java

这里的mMore其实是动画的完成情况,true表示动画未完成,false表示动画已完成或者已取消。我们去看看那里使用了这个方法的返回值,在applyLegacyAnimation()中找到mMore的使用,applyLegacyAnimation()中返回的more决定了动画是否执行完,如果返回为false,那么将会结束动画。

View.java

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

动画的执行流程这里就基本完了,文章篇幅过长,我们来总结一下:

1、当调用View.startAnimation(Animation)时,并没有立即执行动画,而是通过invalidate()层层通过到ViewRootImpl发起一次遍历View树的请求,在接收到下一个(16ms)屏幕信号刷新时才发起遍历View树的绘制操作,从DecorView开始遍历,绘制时会调用draw()方法,如果View有绑定动画则执行applyLegacyAnimation()方法处理相关动画逻辑。

2、在applyLegacyAnimation()里面,先执行初始化initialize(),再通知动画开始onAnimationStart(),然后通过getTransformation()计算动画进度,并且它的返回值和动画是否结束决定是否继续通知ViewRootImpl发起遍历请求,view树绘制,如此重复这个步骤,并且调用applyTransformation()方法执行动画的逻辑,直到动画结束。

其实补间动画仅仅改变的是控件的显示位置,并没有改变控件本身的值,View Animation动画是通过Parent View实现的,在view被draw时Parent View改变它的绘制参数,这样view的大小位置角度等虽然变化了,但是view的实际属性并没有改变。可参考《Android动画篇(三)—— 属性动画ValueAnimator的使用

至此,本文结束!

请尊重原创者版权,转载请标明出处: https://blog.csdn.net/m0_37796683/article/details/90904394 谢谢!

动画系列文章:

1、 Android动画篇(一)—— alpha、scale、translate、rotate、set的xml属性及用法

  • 补间动画的XML用法以及属性详解

2、Android动画篇(二)—— 代码实现alpha、scale、translate、rotate、set及插值器动画

  • 代码动态实现补间动画以及属性详解

3、 Android动画篇(三)—— 属性动画ValueAnimator的使用

  • ValueAnimator的基本使用

4、 Android动画篇(四)—— 属性动画ValueAnimator的高级进阶

  • 插值器(Interpolator)、计算器(Evaluator)、ValueAnimator的ofObject用法等相关知识

5、 Android动画篇(五)—— 属性动画ObjectAnimator基本使用

  • ObjectAnomator的基本使用以及属性详解

6、 Android动画篇(六)—— 组合动画AnimatorSet和PropertyValuesHolder的使用

  • AnimatorSet动画集合和PropertyValuesHolder的使用

以上几篇动画文章是一定要掌握的,写的不好请多多指出!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章