Android動畫實現繪製原理

dead_lee發表於2021-09-09

讀前補充

關於標記位在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@CallSuperpublic 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@Overrideprotected void dispatchDraw(Canvas canvas) {  //balabala
  for (int i = 0; i = 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.javaboolean 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)
    }    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.javapublic void startAnimation(Animation animation) {
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    setAnimation(animation);
    invalidateParentCaches();
    invalidate(true);
}

invalidate(true)函式的主要作用是請求View樹進行重繪,但是具體是怎麼繪製的呢,我們接著往下看。

//View.javavoid 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 

OK,在上面小節提到的函式在這裡出現了,我們看看究竟是怎麼一回事。上面函式大多是設定標記位和判斷標記位,具體什麼意思還不是很清楚,我們重點看下那一小段,這裡的mAttachInfo是在View第一次attach到Window時,ViewRoot傳給自己的子View的。這個AttachInfo之後,會順著佈局體系一直傳遞到最底層的View,下面看看ViewParent到底做了什麼?

//ViewGroup.javapublic 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進行重繪,具體的程式碼如下。

@Overridepublic void invalidateChild(View child, Rect dirty) {
    invalidateChildInParent(null, dirty);
}@Overridepublic ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    checkThread(); //UI執行緒才可以操作UI
    ...
    invalidateRectOnScreen(dirty);    return null;
}private void invalidateRectOnScreen(Rect dirty) {
    ...    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        scheduleTraversals();
    }
}

根據上篇,我們透過分析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的繪製會被跳過,從而直接分發到子Viewpublic RenderNode updateDisplayListIfDirty() {
    ...    //當包含標記位時直接分發
    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
        dispatchDraw(canvas);
        ...
    } else {
        draw(canvas); //文章開頭的6步繪製過程
    }
    ...
}



作者:KuTear
連結:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4686/viewspace-2809923/,如需轉載,請註明出處,否則將追究法律責任。

相關文章