Android 动画实现绘制原理

2,707 阅读6分钟
原文链接: www.kutear.com

读前补充

关于标记位在Android运用的是非常的多,简单的说就是使用二进制中的一位表示一个状态,下面简单的举个栗子。

private int flag = 0;
private static final int NEED_DRAW = 0x1;    //0001; //表示需要绘制
private static final int HAS_ANIMATION = 0x2; //0010;//表示有动画
private static final int HAS_BACKGROUND = 0x4 //0100;//有背景色
private static final int HAS_FORGROUND = 0x8  //1000;//有前景色

void action(){
  if(flag & NEED_DRAW == NEED_DRAW){
     draw();
  }
}

void draw(){
  if(flag & HAS_FORGROUND == HAS_FORGROUND){
    drawForground();
  }
  ...
}

void clearDrawFlag(){
  flag &= ~NEED_DRAW;
}

void setDrawFlag(){
  flag |= NEED_DRAW;
}

上面的思维在View中使用的比较的多,我们只要记住flag & HAS_FORGROUND == HAS_FORGROUND就表示flag存在HAS_FORGROUND标记位,另外,也可以一次判断多个标记的存在情况。

   if(flag & (HAS_FORGROUND | HAS_BACKGROUND) == (HAS_FORGROUND | HAS_BACKGROUND)){
     //前景和背景同时存在
   }

View Animation绘制的流程

在View中,我们知道View的绘制过程是从函数draw(canvas)开始,下面我们来分析一下该函数,根据它的注释部分,我们很容易得到简化版的逻辑代码。

//View.java
@CallSuper
public void draw(Canvas canvas) {
    // Step 1, draw the background, if needed
    drawBackground(canvas);
    // skip step 2 & 5 if possible (common case)
    // Step 3, draw the content  非透明才绘制
    if (!dirtyOpaque) onDraw(canvas);
    // Step 4, draw the children
    dispatchDraw(canvas);
    // Step 6, draw decorations (foreground, scrollbars)
    onDrawForeground(canvas);
}

我们知道,View树的顶层是DecorView,它是FrameLayout的子类,绘制是从根节点开始(RootViewImpl中的dispatchDraw()),根据上面的代码会分发绘制到子View,有子View一定是ViewGroup的子类,所以看看ViewGroupdispatchDraw(canvas)View的该函数是个空实现,因为根本就不需要分发绘制。

//ViewGroup.java
@Override
protected void dispatchDraw(Canvas canvas) {
  //balabala
  for (int i = 0; i < childrenCount; i++) {
          while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
              final View transientChild = mTransientViews.get(transientIndex);
              if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                      transientChild.getAnimation() != null) {
                  more |= drawChild(canvas, transientChild, drawingTime);
              }
              transientIndex++;
              if (transientIndex >= transientCount) {
                  transientIndex = -1;
              }
          }
          int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
          final View child = (preorderedList == null)
                  ? children[childIndex] : preorderedList.get(childIndex);
          if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
              more |= drawChild(canvas, child, drawingTime);  
          }
  }
  //balabala
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

这里代码好多,选择性的看吧,反正逻辑就是调用了函数drawChild(),然而 drawChild()就是直接调用childdraw(Canvas canvas, ViewGroup parent, long drawingTime)函数,这里就是把绘制分发到子View,使得整个View Tree得以绘制。OK,我们接着看到底是怎么绘制的。

//View.java
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime){
    boolean drawingWithRenderNode = mAttachInfo != null
              && mAttachInfo.mHardwareAccelerated
              && hardwareAcceleratedCanvas;
    final Animation a = getAnimation();
    if (a != null) {
        //如果有动画就在这个函数内部请求重绘,return True if the animation is still running
        more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);  
        concatMatrix = a.willChangeTransformationMatrix();
        if (concatMatrix) {
            mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
        }
        transformToApply = parent.getChildTransformation();
    }
    if(drawingWithRenderNode){
        renderNode = updateDisplayListIfDirty();  //其内部调用draw(canvas)
    }

    if (transformToApply != null) {
        if (concatMatrix) {
            if (drawingWithRenderNode) {
                //修改renderNode,应用动画
                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.translate(transX, transY);
            }
            parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
        }

        float transformAlpha = transformToApply.getAlpha();
        if (transformAlpha < 1) {
            alpha *= transformAlpha;
            parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
        }
    }
    return more;
}

private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
        Animation a, boolean scalingRequired) {
    //t为根据动画产生的Transformation{mAlpha,mMatrix},可以控制基本的动画
    boolean more = a.getTransformation(drawingTime, t, 1f);
    if (more) {
      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
      // //这里设置了一个表示该View含有动画的标记位,在后面会使用这个标记位
      parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
      final int left = mLeft + (int) region.left;
      final int top = mTop + (int) region.top;
      //这里请求重绘,根据动画改变的子View的可绘制区域的位置。
      //我们知道View Animation 是没有改变原本View的属性,包括宽高位置。
      parent.invalidate(left, top, left + (int) (region.width() + .5f),
              top + (int) (region.height() + .5f));   
    }
    //balabala
}

//这个函数表示需要重绘的区域
public void invalidate(int l, int t, int r, int b) {
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    invalidateInternal(l - scrollX, t - scrollY, r - scrollX, b - scrollY, true, false);
}

public RenderNode updateDisplayListIfDirty() {
   //....
   if(condition){
     ....
     draw(canvas);
   }
   //....
}

这里调用invalidateInternal()函数就是请求重绘,具体怎么重绘,我们在下面会讲。
上面的过程是View绘制的大体流程,下面看看设置动画的情况。我们设置View动画的入口函数一般都是startAnimation(),下面我们从这里入手。

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

invalidate(true)函数的主要作用是请求View树进行重绘,但是具体是怎么绘制的呢,我们接着往下看。

//View.java
void invalidate(boolean invalidateCache) {
    //这里的mLeft,mRight都是相当于父View来讲的。所以这里的参数就是当前View所在的区域。
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
    //不需要绘制的情况(View不可见&&没有动画等)
    if (skipInvalidate()) {
        return;
    }
    //...
    if(condition/*满足需要绘制的条件*/){
        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);
        }
    }
    //....
}

OK,在上面小节提到的函数在这里出现了,我们看看究竟是怎么一回事。上面函数大多是设置标记位和判断标记位,具体什么意思还不是很清楚,我们重点看下那一小段,这里的mAttachInfo是在View第一次attach到Window时,ViewRoot传给自己的子View的。这个AttachInfo之后,会顺着布局体系一直传递到最底层的View,下面看看ViewParent到底做了什么?

//ViewGroup.java
public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION)
                == PFLAG_DRAW_ANIMATION; //applyLegacyAnimation()中有设置这个标记位
        final int[] location = attachInfo.mInvalidateChildLocation;
        location[CHILD_LEFT_INDEX] = child.mLeft;
        location[CHILD_TOP_INDEX] = child.mTop; //localtion存放的子view的右上角的坐标
        ...

        do {
            View view = null;
            if (parent instanceof View) {
                view = (View) parent;
            }
            //这里意味着如果子View有动画,那么父View也要设置上动画标记位,一直到顶层ViewRootImpl
            if (drawAnimation) {  
                if (view != null) {
                    view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                } else if (parent instanceof ViewRootImpl) {
                    ((ViewRootImpl) parent).mIsAnimating = true;
                }
            }
            // If the parent is dirty opaque or not dirty, mark it dirty with the opaque
            // flag coming from the child that initiated the invalidate
            if (view != null) {
                if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                        view.getSolidColor() == 0) {
                    opaqueFlag = PFLAG_DIRTY;
                }
                if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                    view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
                }
            }

            parent = parent.invalidateChildInParent(location, dirty);
            if (view != null) {
                // Account for transform on current parent
                Matrix m = view.getMatrix();
                if (!m.isIdentity()) {
                    RectF boundingRect = attachInfo.mTmpTransformRect;
                    boundingRect.set(dirty);
                    m.mapRect(boundingRect);
                    dirty.set((int) (boundingRect.left - 0.5f),
                            (int) (boundingRect.top - 0.5f),
                            (int) (boundingRect.right + 0.5f),
                            (int) (boundingRect.bottom + 0.5f));
                }
            }
        } while (parent != null);
    }
}

这里调用了函数invalidateChildInParent(),需要注意的是这里这个函数的实现有两个,一个是ViewGroup中,而另一个是ViewRootImpl

//ViewGroup.java
//总体来讲,这里就是修改一些参数,使其满足当前的ViewGroup,比如坐标等。
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    if ((mPrivateFlags & PFLAG_DRAWN) == PFLAG_DRAWN ||
            (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) {
        if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
                    FLAG_OPTIMIZE_INVALIDATE) {
            dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                    location[CHILD_TOP_INDEX] - mScrollY);
            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
            }

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

            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
                    dirty.setEmpty();
                }
            }
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;

            location[CHILD_LEFT_INDEX] = left;
            location[CHILD_TOP_INDEX] = top;

            if (mLayerType != LAYER_TYPE_NONE) {
                mPrivateFlags |= PFLAG_INVALIDATED;
            }

            return mParent;

        } else {
            mPrivateFlags &= ~PFLAG_DRAWN & ~PFLAG_DRAWING_CACHE_VALID;

            location[CHILD_LEFT_INDEX] = mLeft;
            location[CHILD_TOP_INDEX] = mTop;
            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);
            }

            if (mLayerType != LAYER_TYPE_NONE) {
                mPrivateFlags |= PFLAG_INVALIDATED;
            }

            return mParent;
        }
    }

    return null;
}

而在ViewRootImpl中就是请求整个View Tree进行重绘,具体的代码如下。

@Override
public void invalidateChild(View child, Rect dirty) {
    invalidateChildInParent(null, dirty);
}

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    checkThread(); //UI线程才可以操作UI
    ...
    invalidateRectOnScreen(dirty);

    return null;
}

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

根据上篇对Window/WindowManager和WindowManagerSystem的理解,我们通过分析ViewRootImpl的绘制的时候分析过,函数scheduleTraversals()的逻辑其实是执行一个Runnable,而这个Runnable其实就是去执行函数doTraversal(),而函数doTraversal()会调用performTraversals(),到这里我们发现它开始重绘了。总体来说就是动画的执行会导致整个View Tree重绘,但是Android内部有一些优化,比如一张图片做移动,我们不需要真正的去重新绘制,Android内部提供缓存机制,不会显示的再调用onDraw(canvas)函数。
到这里,动画的执行逻辑大体清楚了。

QA

ViewGroup及其子类onDraw(canvas)没有执行

ViewGroup没有背景时默认是不会执行onDraw(canvas)方法的,具体的原因我们在下面分析。

public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    initViewGroup();
    initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
}

private void initViewGroup() {
    // ViewGroup doesn't draw by default
    if (!debugDraw()) {
        setFlags(WILL_NOT_DRAW, DRAW_MASK); //设置相关的标记位
    }
    ....
}

//#View.java
//可以看见在View里也包含这个设置
//willNotDraw = true ,不绘画
public void setWillNotDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}

//View.java
//WILL_NOT_DRAW = 0x00000080;
//DRAW_MASK = 0x00000080;
void setFlags(int flags, int mask) {
  ....
  int old = mViewFlags;
  mViewFlags = (mViewFlags & ~mask) | (flags & mask);//(flags & mask)= 0x00000080
  int changed = mViewFlags ^ old;
  ....
  if ((changed & DRAW_MASK) != 0) {
            if ((mViewFlags & WILL_NOT_DRAW) != 0) {
                if (mBackground != null
                        || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) {
                    //需要绘制时的标记位,通过这里我们知道,有背景时设置是无效的,这里很容易验证
                    mPrivateFlags &= ~PFLAG_SKIP_DRAW;
                } else {
                    mPrivateFlags |= PFLAG_SKIP_DRAW;  //不要绘制,添加标记位
                }
            } else {
                mPrivateFlags &= ~PFLAG_SKIP_DRAW;
            }
            requestLayout();
            invalidate(true);
  }
  ...
}

//View.java
//这个函数上面有分析,在boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)
//中调用,使用这个函数来绘制View本身部分,通过这里,我们可以看到如果添加了PFLAG_SKIP_DRAW标记
//那么该View的绘制会被跳过,从而直接分发到子View
public RenderNode updateDisplayListIfDirty() {
    ...
    //当包含标记位时直接分发
    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
        dispatchDraw(canvas);
        ...
    } else {
        draw(canvas); //文章开头的6步绘制过程
    }
    ...
}