Androidアニメーションの描画原理を実現

15014 ワード

タイトル:Androidアニメーション実装ペイント原理date:2016-10-05 15:00
本文はKuTear's Blogに発表して、転載して明記してください
プリフェッチ
タグビットについてAndroidでは非常に多く使われていますが、簡単に言えばバイナリの1つを使って1つの状態を表し、次は簡単に栗を挙げます.
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アニメーション描画の流れ
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);
}

ビューツリーの最上位はDecorView、それはFrameLayoutのサブクラスであり、描画はルートノードから(RootViewImpldispatchDraw())であり、上のコードによって描画がサブ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)
    }
    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、上の節で述べた関数がここに現れました.いったいどういうことなのか見てみましょう.上の関数の多くはタグビットの設定とタグビットの判断であるが,具体的にどういう意味かはまだはっきりしていないが,ここでのmAttachInfoViewが初めてattachしてWindowに着いたとき,View Rootが自分の子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()ここでこの関数の実装は2つあり,1つはViewGroupのうち,もう1つは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()の論理は実は1つRunnableを実行しているが、これRunnable実は関数を実行しているdoTraversal()、関数doTraversal()が呼び出されるperformTraversals()、ここまで再描画が始まっていることが分かった.全体的にアニメーションの実行は全体View Tree再描画を招くが、Android内部には1枚の画像を移動するなどの最適化があり、本格的に再描画する必要はなく、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     
    }
    ...
}