View动画使用
view动画有缩放、旋转、平移、透明度等,都是继承于Animation类。我们掌握了一个类型的原理其他原理基本一致。都是通过Transformation 和 Matrix 实现各种各样炫酷的动画。
ScaleAnimation scaleAnimation = new ScaleAnimation(0,1,0,1);
scaleAnimation.setDuration(1000);
scaleAnimation.setFillAfter(true);
TextView textView = new TextView(this);
textView.startAnimation(scaleAnimation);
我们一般都是通过以上进行使用View动画。我们看下startAnimation的流程:
源码分析
View.startAnimation
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
我们看到startAnimation里又调用四个方法,首先设置起始帧,然后将animation设置给当前view。
public void setAnimation(Animation animation) {
mCurrentAnimation = animation;
}
protected void invalidateParentCaches() {
if (mParent instanceof View) {
((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
}
}
ViewGroup的标志设置了PFLAG_INVALIDATED,所以ViewGroup发生了重绘。
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) {
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);
}
}
内部起始调用了ViewGroup的invalidateChild,跟进去看:
public final void invalidateChild(View child, final Rect dirty) {
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
parent = parent.invalidateChildInParent(location, dirty);
} while (parent != null);
一个具体的view的parent是ViewGroup,ViewGroup的parent也是ViewGroup,而ViewGroup最顶层的是DecorView,而DecorView的parent是ViewRootImpl,所以最终会走到ViewRootImpl的invalidateChildInParent里去了。至于一个界面的ViewRootImpl是根布局需要看下一启动流程呀。
我们看下:
ViewRootImpl.invalidateChildInParent
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
if (dirty == null) {
invalidate();
return null;
} else if (dirty.isEmpty() && !mIsAnimating) {
return null;
}
invalidateRectOnScreen(dirty);
return null;
}
首先我们看最后return null了,也就是在这里,while循环停止了,找到了ViewRootImpl,dirty是上层传过来的,肯定不为空,所以会执行invalidateRectOnScreen
private void invalidateRectOnScreen(Rect dirty) {
scheduleTraversals();
}
跟到这里就差不多了,我们知道View的绘制流程,基本就是从scheduleTraversals开始,将performTraversals封装到Runnable里,然后扔到Choreographer 的待执行队列里,这些执行队列将再最近的一个16.6ms的屏幕刷新时间来的时候执行。performTraversals就是执行view绘制的三大操作:测量、布局、绘制的发起者。
View树里不管哪个view发起了布局、绘制请求,都会到顶部的ViewRootImpl里的scheduleTraversals开始,然后在最近的一个屏幕信号到了,通过ViewRootImpl的performTraversals从跟布局开始去遍历view树执行测量、布局、绘制三大操作。这也是为什么我们布局不能太深的原因,因为每一次刷新绘制都会走到ViewRootImpl里,然后再层层遍历找到改变的view去执行响应的布局或者绘制操作 。
所以我们在调用startAnimation只是找到顶部的ViewRootImpl通过DecorView来一层层往下分发做到更新自己的View。ViewRootImpl会调用performDraw,performDraw会调用draw方法,然后调用View的draw方法。如下:
private void performDraw() {
boolean canUseAsync = draw(fullRedrawNeeded);
}
private boolean draw(boolean fullRedrawNeeded) {
...
if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,
scalingRequired, dirty, surfaceInsets)) {
return false;
}
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
...
mView.draw(canvas);
...
}
最终调用到每个 View 的 draw 方法绘制每个具体的 View,绘制基本上可以分为六个步骤。
public void draw(Canvas canvas) {
...
// Step 1, draw the background, if needed
if (!dirtyOpaque) {
drawBackground(canvas);
}
...
// Step 2, save the canvas' layers
saveCount = canvas.getSaveCount();
...
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 5, draw the fade effect and restore layers
canvas.drawRect(left, top, right, top + length, p);
...
canvas.restoreToCount(saveCount);
...
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
}
View的绘制流程大体是:ViewGroup调用draw方法,如果当前需要绘制就去调自己的onDraw方法,然后如果有子view就去调用dispatchDraw,将绘制事件通知给子view,ViewGroup重写了dispatchDraw,调用了drawChild,而drawchild调用了子view的draw(Canvas, ViewGroup, long),而这个方法又去调用draw(Canvas)达到了遍历的目的。
我们在draw(Canvas, ViewGroup, long)找到了动画的执行过程
draw(Canvas canvas, ViewGroup parent, long drawingTime)
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
final Animation a = getAnimation();
if (a != null) {
more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
}
return more;
}
还记得我们调用 View.startAnimation(Animation) 时将传进来的 Animation 赋值给 mCurrentAnimation 了么。
所以当时传进来的 Animation ,现在拿出来用了,那么动画真正执行的地方应该也就是在 applyLegacyAnimation() 方法里了(该方法在 android-22 版本及之前的命名是 drawAnimation)。
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 (more) {
parent.invalidate(left, top, left + (int) (region.width() + .5f),
top + (int) (region.height() + .5f);
}
return more;
}
这下确定动画真正开始执行是在什么地方了吧,都看到 onAnimationStart() 了,也看到了对动画进行初始化,以及调用了 Animation 的 getTransformation,这个方法是动画的核心,再跟进去看看:
public boolean getTransformation(long currentTime, Transformation outTransformation) {
if (mStartTime == -1) {
//记录动画的第一帧时间
mStartTime = currentTime;
}
final long startOffset = getStartOffset();
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 = !expired;
if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
//根据插值器计算实际的动画进度
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
applyTransformation(interpolatedTime, outTransformation);
}
return mMore;
}
这个方法里做了几件事:
记录动画第一帧的时间
根据当前时间到动画第一帧的时间这之间的时长和动画应持续的时长来计算动画的进度
把动画进度控制在 0-1 之间,超过 1 的表示动画已经结束,重新赋值为 1 即可
根据插值器来计算动画的实际进度
调用 applyTransformation() 应用动画效果
所以,到这里我们已经能确定 applyTransformation() 是什么时候回调的,动画是什么时候才真正开始执行的。那么 Q1 总算是搞定了,Q2 也基本能理清了。因为我们清楚, applyTransformation() 最终是在绘制流程中的 draw() 过程中执行到的,那么显然在每一帧的屏幕刷新信号来的时候,遍历 View 树是为了重新计算屏幕数据,也就是所谓的 View 的刷新,而动画只是在这个过程中顺便执行的。
但是这只是执行第一帧动画呀,其他帧动画呢,细心的朋友可能发现了,我们getTransformation有个boolean值,他的返回值代表了动画是否完成,我们是在applyLegacyAnimation调用的getTransformation,通过它的boolean值来做了什么:
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
Animation a, boolean scalingRequired) {
final Transformation t = parent.getChildTransformation();
boolean more = a.getTransformation(drawingTime, t, 1f);
//通过boolean值来确认是否需要
if (more) {
parent.invalidate(left, top, left + (int) (region.width() + .5f),
top + (int) (region.height() + .5f);
}
return more;
}
当动画如果还没执行完,就会再调用 invalidate() 方法,层层通知到 ViewRootImpl 再次发起一次遍历请求,当下一帧屏幕刷新信号来的时候,再通过 performTraversals() 遍历 View 树绘制时,该 View 的 draw 收到通知被调用时,会再次去调用 applyLegacyAnimation() 方法去执行动画相关操作,包括调用 getTransformation() 计算动画进度,调用 applyTransformation() 应用动画。
也就是说,动画很流畅的情况下,其实是每隔 16.6ms 即每一帧到来的时候,执行一次 applyTransformation(),直到动画完成。所以这个 applyTransformation() 被回调多次是这么来的,而且这个回调次数并没有办法人为进行设定。
总结
综上,我们稍微整理一下:
-
首先,当调用了 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() 操作。