View 动画源码解析

Posted by Allen Vork on August 12, 2018

Synopsis

前面在 Android 动画基础中已经简单讲了 View 动画的基本用法,本文将通过一个简单的例子来介绍 View 动画的内部原理。

Sample

val alphaAnim = AlphaAnimation(0f, 1f)
alphaAnim.duration = 3000
button.startAnimation(alphaAnim)

Let’s make it brief

我们先直接从流程图上看从 startAnimation 开始做了什么: 它会调用 View 的 invalidate() 方法来进行刷新,并将该 View 的大小和位置传给 invalidateInternal() 方法,然后调用 parent 的 invalidateChild(),该方法会循环调用 parent.invalidateChildInParent 直到 parent 为空。首次 parent 为自身,返回值为自己的 parent。我们再来看下这个方法的流程图: 我们印象中 View 的 parent 一定是 ViewGroup,毕竟顶级 View 为 DecorView,它继承自 FrameLayout。那么 parent.invalidateChildInParent 就会一直调用的是 ViewGroup 中的该方法。而该方法主要是根据传过来的 View 的大小和位置以及自己的大小来计算出所需要绘制的区域,一直回溯到最上层得到最终所需要绘制的区域。 里面没有任何跟动画相关的操作,那么它是如何触发的呢,其实 DecorView 是会被添加到 ViewRootImpl 中,最终执行的是它的方法。它内部会将一个包含 performTraversals() 方法的 Runnable 传给 Choreographer,等到系统发出刷新信号后就会执行。该方法主要是遍历 View 树来执行 onMeasure 等方法

View.startAnimation()

首先我们来看下 View 的 startAnimation 到底做了什么:

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

我们来看下 setAnimation(animation) 方法:

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();
    }
}

这里是将我们创建的 animation 对象赋值给 View 的 Animation 对象,并没有具体的动画逻辑,我们再来看下 invalidateParentCaches():

/**
 * Used to indicate that the parent of this view should clear its caches. This functionality
 * is used to force the parent to rebuild its display list (when hardware-accelerated),
 * which is necessary when various parent-managed properties of the view change, such as
 * alpha, translationX/Y, scrollX/Y, scaleX/Y, and rotation/X/Y. This method only
 * clears the parent caches and does not causes an invalidate event.
 *
 * @hide
 */
protected void invalidateParentCaches() {
     if (mParent instanceof View) {
         ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
      }
}

从注释可以看出,这个方法是给父 View 增加一个标志位,用于告诉它要清理自己的缓存。当多个由父 View 管理的该子 View 的属性(如 alpha, translationX/Y 等)变化时会用到。我们再往下看:

    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 (skipInvalidate()) { // 当 View 不为 VISIBLE 且没有动画时,直接不绘制
                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);
                // 调用 parent 的 invalidateChild 来刷新
                p.invalidateChild(this, damage);
            }

            // ...

可以看出它调用了 ViewGroup 的 invalidateChild()方法:

    public final void invalidateChild(View child, final Rect dirty) {
        // ...
        ViewParent parent = this;
        if (attachInfo != null) {
            // 如果子 View 有动画,则要将该标志位复制给自己和自己的 parent 以确保刷新请求能通过
            final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;

            // ...
            do {
                View view = null;
                if (parent instanceof View) { // 从这里看出在 View 树中,某些 parent 可能不为 View
                    view = (View) parent;
                }

                if (drawAnimation) {
                    if (view != null) {
                        view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    } else if (parent instanceof ViewRootImpl) { 
                        // 看出 View 的某个 parent 为 ViewRootImpl(不是 ViewGroup)
                        ((ViewRootImpl) parent).mIsAnimating = true;
                    }
                }

                // ...

                // 循环调用该方法,直到 parent 为 null。循环开始时,parent 为自身。
                // location 为 child 左上角与自己的左上角距离
                // dirty 为所要绘制的区域[0, 0, width, height]
                parent = parent.invalidateChildInParent(location, dirty);
                // invalidateChildInParent 执行完后会得到新的 dirty 和 location
                if (view != null) {
                    // Account for transform on current parent
                    Matrix m = view.getMatrix(); // 获得当前的 matrix
                    if (!m.isIdentity()) {
                        RectF boundingRect = attachInfo.mTmpTransformRect;
                        // boundingRect 即为当前所绘制的区域大小,即[0, 0, width, height]
                        boundingRect.set(dirty); 
                        // 如果该 view 有做 matrix 变换,如 matrix.setScale(2f, 1f)
                        // 即将宽度变为2倍,那么要将 boundingRect 变为[0, 0, 2 * widht, height]
                        m.mapRect(boundingRect);
                        // 然后将真实所应绘制的区域设置给 dirty
                        dirty.set((int) Math.floor(boundingRect.left),
                                (int) Math.floor(boundingRect.top),
                                (int) Math.ceil(boundingRect.right),
                                (int) Math.ceil(boundingRect.bottom));
                    }
                }
            } while (parent != null);
        }
    }

可以看出它会获得当前 view 的绘制区域dirty:[0, 0, width, height],以及该 view 相对父 view 的距离 location:[left, top],里面循环调用 invalidateChildInParent(location, dirty)。我们进去看看:

    /**
     * This implementation returns null if this ViewGroup does not have a parent,
     * if this ViewGroup is already fully invalidated or if the dirty rectangle
     * does not intersect with this ViewGroup's bounds.
     */
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
            // either DRAWN, or DRAWING_CACHE_VALID
            if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
                    != FLAG_OPTIMIZE_INVALIDATE) {
                // 用 dirty 和 location计算出绘制区域相对于 parent 的坐标 
                dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                        location[CHILD_TOP_INDEX] - mScrollY);
                // 不裁减(即子 view 和 parent 有重叠的部分也需要进行绘制)则取并集得到新的绘制区域
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                    dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
                }

                final int left = mLeft;
                final int top = mTop;

                // 裁减的话,则取交集(仅仅绘制子 View 内有效的区间)
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                    if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
                        dirty.setEmpty();
                    }
                }

                location[CHILD_LEFT_INDEX] = left;
                location[CHILD_TOP_INDEX] = top;
            } else {

                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                    dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
                } else {
                    // in case the dirty rect extends outside the bounds of this container
                    dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
                }
                location[CHILD_LEFT_INDEX] = mLeft;
                location[CHILD_TOP_INDEX] = mTop;

                mPrivateFlags &= ~PFLAG_DRAWN;
            }
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            if (mLayerType != LAYER_TYPE_NONE) {
                mPrivateFlags |= PFLAG_INVALIDATED;
            }

            return mParent;
        }

        return null;
    }

可以看出它所做的事情就是通过是否要进行裁减来决定绘制区域,这个函数是返回当前 ViewGroup 的 parent 的。里面还没有看到具体的动画逻辑,由于 invalidateChildInParent 会一直循环执行,一直到 parent 为 null 为止,而 ViewGroup 的 parent 也是 ViewGroup,那么无论循环多少次都是调用一样的方法,都没有跟具体的动画逻辑有关系,那么动画到底是在哪里触发的呢?
我们知道,我们的布局是通过 setContentView() 时添加到 DecorView 中的,那么 DecorView 就是 View 树的根布局,而 DecorView 其实是会在 WindowManagerGlobal 的 addView 方法被添加到 ViewRootImpl 中,这个才是真正的根布局(虽然并没有继承 ViewGroup)。我们来看下 ViewRootImpl 的 invalidateChildInParent 方法:

    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        // ...
        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }
        // ...
        invalidateRectOnScreen(dirty);

        return null;
    }

从 ViewGroup 的 invalidateChildInParent 方法里看出来 dirty 是不为空的,那么会直接走到 invalidateRectOnScreen 方法中:

    private void invalidateRectOnScreen(Rect dirty) {
        // ...
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
    }

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            // ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            // ...
        }
    }

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

    void doTraversal() {
        if (mTraversalScheduled) {
            // ...
            performTraversals();
            // ...
        }
    }

可以看出它的目的是将 performTraversals() 方法封装到 Runnalbe 里,然后传给 Choreographer。我们来看下传给它后做了什么:

    /**
     * Posts a callback to run on the next frame.
     * <p>
     * The callback runs once then is automatically removed.
     * </p>
     *
     * @param callbackType The callback type.
     * @param action The callback action to run during the next frame.
     * @param token The callback token, or null if none.
     *
     * @see #removeCallbacks
     * @hide
     */
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }

从注释可以看出,这个将 Runnable 传进来是为了在屏幕绘制下一帧的时候去执行。也就是我们调用 start 启动动画后并不是直接执行的,而是在屏幕绘制下一帧的时候才会执行。这个方法最终会调用到:

    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            
            // 会将 Runnable 封装到 CallbackRecord 中,然后存入队列中
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                scheduleFrameLocked(now);
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

Runnable 被存入到 Choreographer 的待执行队列中,当屏幕绘制信号(安卓手机屏幕刷新频率为60fps,也就是16.7ms屏幕会刷新一次)到来时会被执行,这样 ViewRootImpl 的 performTranversals() 就会执行。这个方法是从 DecorView 开始遍历 View 树来执行 onMeasure 等操作的。由于每一次页面刷新都会执行 ViewRootImpl 的 performTraversal() 操作来遍历 View 树,找到发生改变的 View 来执行 onMeasure 等操作,所以我们写布局时要尽量减少层级。

我们来总结下调用 startAnimation 后的流程: 可以回顾下前面的流程图

  1. 调用 View.StartAnimation(Animation) 方法后,会调用 setAnimation() 将这个 animation 会作为 View 的全局变量保存起来
  2. 然后调用 View 的 invalidate() 方法,它会调用 parent.invalidateChild() 方法来调到 ViewGroup 中的该方法。
  3. 这时 ViewGroup 会循环执行parent = parent.invalidateChildInParent(location, dirty) 直到 parent 为空。

整个过程就处理完了,但 ViewGroup 中的 invalidateChildInParent 都是在处理一些参数,并没有和动画有直接联系,那动画到底是在哪里处理的?想到整个 View 树的 parent 不仅仅是 ViewGroup,它的顶层是 ViewRootImpl。ViewRootImpl 中的 invalidateChildInParent() 才是核心所在。它会将一个会执行 performTraversals() 的 Runnable 传到 Choreographer,当屏幕绘制下一帧的时候就会执行。真正跟动画相关的代码在 performTraversals() 中。该操作会执行测量,布局和绘制。由于动画相关代码在 draw() 里,所以我们直接进入 View 的 draw() 方法:

    public void draw(Canvas canvas) {
        // ...

        // Step 1, draw the background, if needed
        int saveCount;
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // 如果可以的话,跳过第2和5步
        // ...
        if (!verticalEdges && !horizontalEdges) {
            // Step 3,开始调用 onDraw() 方法来绘制内容
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, 绘制子 View。这个方法的具体实现在 ViewGroup 中
            dispatchDraw(canvas);
            drawAutofilledHighlight(canvas);
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done...
            return;
        }

        // 后面就将 2~6 步骤完整走一遍
    }

draw() 方法做了6步:

  1. 绘制背景
  2. 如果有必要的话,保存 canvas 图层来为 fading 做准备
  3. 调用 onDraw(canvas) 绘制 View 的内容
  4. dispatchDraw(canvas) 绘制子 View,这一步具体是由 ViewGroup 实现
  5. 如果有必要的话,绘制 fading 边界并保存图层
  6. 绘制装饰(如 scrollbars)

第四步,它会调用 child.draw(Canvas canvas, View child, long drawingTime) 方法来触发子 View 的 draw(Canvas, View, long) 方法。 我们来看下这个方法:

    /**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        // ...
        boolean more = false;
        // ...
        // 获取在启动动画时传进来的 Animation 对象
        final Animation a = getAnimation();
        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            // ...
        } 
        // ...
        return more;
    }

可以看到这个方法中获取了之前传给 View 的 animation,然后传给 applyLegacyAnimation 方法:

    /**
     * Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
     * case of an active Animation being run on the view.
     */
    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();
        // 获取动画的 Transformation, 返回值标志动画有没有做完,true 代表没有完成
        boolean more = a.getTransformation(drawingTime, t, 1f);

        // ...

        // 动画未执行完
        if (more) {
            if (!a.willChangeBounds()) { // 动画是否会改变大小,alpha 动画会返回 false, 其余动画返回 true
                if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
                        ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
                    parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
                } else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
                    // The child need to draw an animation, potentially offscreen, so
                    // make sure we do not cancel invalidate requests
                    parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    parent.invalidate(mLeft, mTop, mRight, mBottom);
                }
            } else {
                if (parent.mInvalidateRegion == null) {
                    parent.mInvalidateRegion = new RectF();
                }
                final RectF region = parent.mInvalidateRegion;
                a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
                        invalidationTransform);

                // The child need to draw an animation, potentially offscreen, so
                // make sure we do not cancel invalidate requests
                parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

                final int left = mLeft + (int) region.left;
                final int top = mTop + (int) region.top;

                // 内部调用 invalidateInternal 方法,然后将前面讲过的流程再走一遍,一直循环到动画结束 
                parent.invalidate(left, top, left + (int) (region.width() + .5f),
                        top + (int) (region.height() + .5f));
            }
        }
        return more;
    }

可以看出这里真正开始初始化,处理动画了。我们来看下 getTransformation 方法:

    /**
     * Gets the transformation to apply at a specified point in time. Implementations of this
     * method should always replace the specified Transformation or document they are doing
     * otherwise.
     *
     * @param currentTime Where we are in the animation. This is wall clock time.
     * @param outTransformation A transformation object that is provided by the
     *        caller and will be filled in by the animation.
     * @return True if the animation is still running
     */
    public boolean getTransformation(long currentTime, Transformation outTransformation) {
        if (mStartTime == -1) { // 记录动画开始执行的时间
            mStartTime = currentTime;
        }

        final long startOffset = getStartOffset(); // 默认为0
        final long duration = mDuration;
        float normalizedTime;
        if (duration != 0) {
            // 根据当前时间计算动画的进度的百分比
            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                    (float) duration;
        } else {
            // time is a step-change with a zero duration
            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
        }

        final boolean expired = normalizedTime >= 1.0f || isCanceled();
        // mMore 用来判断后面还有没有动画要执行。true 代表当前动画已经执行完了。
        mMore = !expired;

        if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

        if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
            if (!mStarted) { // 动画还没有 start()
                fireAnimationStart();
                mStarted = true;
                if (NoImagePreloadHolder.USE_CLOSEGUARD) {
                    guard.open("cancel or detach or getTransformation");
                }
            }

            if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

            if (mCycleFlip) { 
                normalizedTime = 1.0f - normalizedTime;
            }

            // 将当前动画的进度传给 interpolator,获取实际的动画进度
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);

            // 执行动画
            applyTransformation(interpolatedTime, outTransformation);
        }

        // 动画执行完了
        if (expired) {
            if (mRepeatCount == mRepeated || isCanceled()) { // 如果动画被取消或者动画重复也做完了
                if (!mEnded) {
                    mEnded = true;
                    guard.close();
                    fireAnimationEnd();
                }
            } else { // 动画需要重复播放
                if (mRepeatCount > 0) {
                    mRepeated++;
                }

                if (mRepeatMode == REVERSE) { // 动画反转
                    mCycleFlip = !mCycleFlip;
                }

                mStartTime = -1;
                mMore = true;

                fireAnimationRepeat();
            }
        }

        if (!mMore && mOneMoreTime) {
            mOneMoreTime = false;
            return true;
        }

        return mMore;
    }

可以看出逻辑比较简单,就是计算当前动画的进度,然后将进度控制在0-1之间,将动画进度传给插值器获得动画的真实进度,然后执行。
它的返回值是用来判断动画是否已经执行完,如果没有执行完成的话,applyLegacyAnimation() 中会调用 parent.invalidate() 方法,它会调用 View.invalidateInternal() 方法,层层通知到 ViewRootImpl 然后再次发起一次遍历请求,等下一帧信号来的时候,就会执行 performTranversal() 遍历 View 树,调用 draw() 方法,然后调用到 applyLegacyAnimation() 来执行动画操作,这些就是前面讲过的。

conclusion

  • 调用 startAnimation() 方法将 Animation 传给 View,然后调用 View 的 invalidate() 方法一层层的往上通知到 ViewRootImpl,它会将布局绘制操作封装到一个 Runnable 然后传给 Choreographer,等受到处置信号时,就会执行这个 Runnable 来绘制下一帧。
  • 这个布局绘制操作会从 DecorView 开始往下遍历 View 树,并调用 draw() 方法,然后会调用 applyLegacyAnimation() 方法来处理动画。
  • applyLegacyAnimation() 方法会调用 Animation 类的 getTransformation() 方法,它是用来计算当前动画的进度并使用 applyTransformaion() 方法来执行动画。getTransformation() 的返回值标志动画是否完成,没有完成的话,会再次调用“invalidate”方法,重复上面步骤1,等下次受到垂直信号时,再次执行动画,直到动画结束。

references