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 后的流程: 可以回顾下前面的流程图
- 调用 View.StartAnimation(Animation) 方法后,会调用 setAnimation() 将这个 animation 会作为 View 的全局变量保存起来
- 然后调用 View 的 invalidate() 方法,它会调用 parent.invalidateChild() 方法来调到 ViewGroup 中的该方法。
- 这时 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步:
- 绘制背景
- 如果有必要的话,保存 canvas 图层来为 fading 做准备
- 调用 onDraw(canvas) 绘制 View 的内容
- dispatchDraw(canvas) 绘制子 View,这一步具体是由 ViewGroup 实现
- 如果有必要的话,绘制 fading 边界并保存图层
- 绘制装饰(如 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,等下次受到垂直信号时,再次执行动画,直到动画结束。