前言
补间动画移动后,点击事件的响应为什么还在原来的位置?
那今天我们就来从源码解析原理
一、补间动画
补间动画可以在一个视图容器内执行一系列简单变换(具体的变换步骤有:位置、大小、旋转、透明度);
我们可以通过平移、旋转、缩放、透明度等API进行具体的操作;
补间动画的实现方式可以通过 XML或通过Android代码两种方式 去定义;
1、xml方式实现
文件名:animator_translate.xml
- <?xml version="1.0" encoding="utf-8"?>
- <translate
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:fromXDelta="0"
- android:fromYDelta="0"
- android:toYDelta="0"
- android:toXDelta="200"
- android:duration="500"
- android:fillAfter="true">
- </translate>
代码加载xml文件获取动画
- //加载动画
- Animation animation = AnimationUtils.loadAnimation(this, R.anim.animator_translate);
- //执行动画
- testBtn.startAnimation(animation);
2、代码方式实现
- TranslateAnimation translateAnimation = new TranslateAnimation(0,200,0,0);
- translateAnimation.setDuration(500);//动画执行时间
- translateAnimation.setFillAfter(true);//动画执行完成后保持状态
- //执行动画
- testBtn.startAnimation(translateAnimation);
二、补间动画原理解
1、startAnimation
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);
2、invalidate
- //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的时候结束循环;
- invalidateChildInParent方法中,只要条件成立就会返回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;
- 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()循坏也会停止。
3、scheduleTraversals()
- 接着分析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();
4、doTraversal()
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里去执行相应的布局和绘制操作;
- 所以在调用View.startAnimation(rotateAnimation)后,并没有立即执行动画,而是做了一下变量初始化操作,将View和Animation绑定起来,调用重绘操作,内部层层寻找mPartent,最终在ViewRootImpl的scheduleTraversals()发起一个遍历View树的请求,在最近一个屏幕信号刷新到来时执行这个请求,调用performTraversals()从根布局去遍历View树。
5、draw
- 我们继续分析draw方法是怎样将动画绘制的且动画是怎样动起来的呢?
- invalidate最终会调用到ViewRootImpl 注册Choreographer的回调;
- 在下一次VSYN信号到来时 会调用ViewRootImpl performTraversals 最终会调用到View的draw方法。
- boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
- ...
- //清除上次动画保存的Transformation
- if ((parentFlags & ViewGroup.FLAG_CLEAR_TRANSFORMATION) != 0) {
- parent.getChildTransformation().clear();
- parent.mGroupFlags &= ~ViewGroup.FLAG_CLEAR_TRANSFORMATION;
- }
- ......
- final Animation a = getAnimation();
- if (a != null) {
- //根据当前时间计算当前帧的动画,more表示是否需要执行更多帧的动画
- more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
- concatMatrix = a.willChangeTransformationMatrix();
- if (concatMatrix) {
- mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
- }
- //拿到当前帧需要的变换 ,这个值会在applyLegacyAnimation中进行设置
- transformToApply = parent.getChildTransformation();
- }
- ....
- if (transformToApply != null) {
- if (concatMatrix) {
- if (drawingWithRenderNode) {
- renderNode.setAnimationMatrix(transformToApply.getMatrix());
- } else {
- // Undo the scroll translation, apply the transformation matrix,
- // then redo the scroll translate to get the correct result.
- canvas.translate(-transX, -transY);
- canvas.concat(transformToApply.getMatrix());//在这里调用canvas的concat方法,实现最终的平移效果 (做矩阵相乘)
- canvas.translate(transX, transY);
- }
- //标记需要清除Tranformation
- parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
- }
- float transformAlpha = transformToApply.getAlpha();
- if (transformAlpha < 1) {
- alpha *= transformAlpha;
- parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
- }
- }
- ...
- }
- 调用applyLegacyAnimation根据当前时间来计算当前帧的动画同时返回值表示动画是否还没播放完成;
- 拿到当前帧需要的变换transformToApply;
- 调用canvas.concat 方法 实现最终的平移效果 (做矩阵相乘) 这样我们就将最终的平移想过画到canvas上面了解决了如何完成绘制的问题;
- 接下来继续分析applyLegacyAnimation是如何计算当前的帧的动画的。
6、applyLegacyAnimation
- private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
- Animation a, boolean scalingRequired) {
- ...
- //获取Transformation 每个ViewGroup中的子View共同使用一个Transformation 为了多个View有动画时频繁创建多个Transformation
- //这个和在draw方法中取出的transformToApply是一个对象 就是最终应用到Canvas上的Transform
- final Transformation t = parent.getChildTransformation();
- //调用Animation的getTransformation方法来根据当前时间计算Transformation 这个对象的值最终会由getTransformation方法中进行赋值
- boolean more = a.getTransformation(drawingTime, t, 1f);
- invalidationTransform = t;
- ...
- //如果动画还没有播放完成 需要让动画循环起来 实际上是继续调用invalidate
- if (more) {
- if (parent.mInvalidateRegion == null) {
- parent.mInvalidateRegion = new RectF();
- }
- final RectF region = parent.mInvalidateRegion;
- //调用Animation 的getInvalidateRegion来根据invalidationTransform计算 parent的invalidateRegion
- 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;
- //调用invalidate执行下一次绘制请求,这样动画就动起来了
- parent.invalidate(left, top, left + (int) (region.width() + .5f),
- top + (int) (region.height() + .5f));
- }
- }
- 在applyLegacyAnimation方法中会再次调用parent.invalidate注册一个Choreographer回调,下一次VSYN后又会调用draw方法.这样就循环起来了;
- 只有当more为false时 表示动画播放完成了 这时候就不会invalidate了;
- 继续看getTransformation是如何计算Transformation的。
- //Animation.java
- //返回值表示动画是否没有播放完成 并且需要计算outTransformation 也就是动画需要做的变化
- public boolean getTransformation(long currentTime, Transformation outTransformation) {
- if (mStartTime == -1) {
- mStartTime = currentTime;//记录第一帧的时间
- }
- if (duration != 0) {
- normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) / //计算运行的进度(0-1) (当前时间-开始时间+偏移量)/动画总时长
- (float) duration;
- }
- final boolean expired = normalizedTime >= 1.0f || isCanceled(); //判断动画是否播放完成 或者被取消
- mMore = !expired;
- if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f); //处理最大值
- final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);//根据插值器计算的当前动画运行进度
- applyTransformation(interpolatedTime, outTransformation);//根据动画进度 计算最终的outTransformation
- return mMore;
- }
- 记录动画第一帧的时间;
- 根据当前时间到动画第一帧的时间这之间的时长和动画应持续的时长来计算动画的进度;
- 把动画进度控制在 0-1 之间,超过 1 的表示动画已经结束,重新赋值为 1 即可;
- 根据插值器来计算动画的实际进度;
- 调用 applyTransformation() 应用动画效果。
- //applyTransformation每种类型的动画都有自己的实现 这里以位移动画为例
- //TranslateAnimation.java
- @Override
- protected void applyTransformation(float interpolatedTime, Transformation t) {
- //Transformation可以理解成 存储View的一些变换信息,将变化信息保存到成员变量matrix中
- float dx = mFromXDelta;
- float dy = mFromYDelta;
- if (mFromXDelta != mToXDelta) {
- dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);//计算X方向需要移动的距离
- }
- if (mFromYDelta != mToYDelta) {
- dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);//计算Y方向需要移动的距离
- }
- t.getMatrix().setTranslate(dx, dy); //将最终的结果设置到Matrix上面去
- }
- 至此计算完最终的变化然后应用到了Transformation的Matix上,会在draw方法中拿到该Transformation并应用到Canvas上,调用canvas.concat,进行平移动画;
- 当动画如果还没执行完,就会再调用 invalidate() 方法,层层通知到 ViewRootImpl 再次发起一次遍历请求,当下一帧屏幕刷新信号来的时候;
- 再通过 performTraversals() 遍历 View 树绘制时,该 View 的 draw 收到通知被调用时;
- 会再次去调用 applyLegacyAnimation() 方法去执行动画相关操作,包括调用 getTransformation() 计算动画进度,调用 applyTransformation() 应用动画。
7、动画总结
- 当调用View.startAnimation(Animation)时,并没有立即执行动画,而是通过invalidate()层层通过到ViewRootImpl发起一次遍历View树的请求,在接收到下一个(16ms)屏幕信号刷新时才发起遍历View树的绘制操作,从DecorView开始遍历,绘制时会调用draw()方法,如果View有绑定动画则执行applyLegacyAnimation()方法处理相关动画逻辑;
- 在applyLegacyAnimation()里面,先执行初始化initialize(),再通知动画开始onAnimationStart(),然后通过getTransformation()计算动画进度,并且它的返回值和动画是否结束决定是否继续通知ViewRootImpl发起遍历请求,view树绘制,如此重复这个步骤,并且调用applyTransformation()方法执行动画的逻辑,直到动画结束。
总结
至于未来会怎样,要走下去才知道,反正路还很长,天总会亮;
加油老铁们!
本文转载自微信公众号「Android开发编程」