Androidでの描画メカニズム

14636 ワード

実はAndroidシステムの描画はほとんど最下位で完了していることを知っています(Nativeを呼び出す方法はCanvasクラスを参照してください).ここでは、Androidのframeworkという層での描画メカニズムについてお話ししたいと思います.あまり底辺のものには触れませんが、これは今のところ私もあまり深く研究していません.
一、Viewの描画方法
View#drawメソッドは、サブクラスが通常このメソッドを書き換える必要がない最も基本的な描画メカニズムを提供します.ソースコードを表示することで、Viewのdrawでは通常、以下のことをする必要があります.
1、自分の背景を描きます.もしあれば、背景は常に一番後ろにあるので、先に描きます.
2,必要であればcanvasのlayerを保存してグラデーション効果を描く準備をします.例えばalphaアニメーションなどがあります.
3,Viewの内容を描くということは,実はonDrawメソッドを呼び出し,サブクラスが自分の内容を描くことができるようにすることである.
4、自分のchildを描いて、具体的にどのように子供を描いて、ViewGroupは相応の方法を書き直します.ベースクラスのViewは呼び出しというchildを描く方法だけで、もちろんこの方法はViewの中にあり、何もしないはずです.
5、必要に応じてグラデーション効果を描き、保存したcanvasレイヤを復元します.
6、scrollbarなどの他の要素を描画します.
通常、Viewではこれらのことをしなければならず、順序を変えることはできません.
コードを分析します.
final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
これらの文は、現在のViewが不透明であるかどうか、またはdirty(現在のViewが再描画する必要があるかどうかを示す)であるかどうかを判断することであり、いくつかのFLAGに基づいて判断される.dirtyOpaqueがtrueの場合、背景を描き、onDrawというメソッドも呼び出され、サブクラスではこのメソッドを書き換えることができます.ここでは、ViewGroupの場合、デフォルトではonDrawは呼び出されません.デフォルトは透明なので、描画する必要はありません.
if (!dirtyOpaque) {
            final Drawable background = mBGDrawable;
            if (background != null) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;

                if (mBackgroundSizeChanged) {
                    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
                    mBackgroundSizeChanged = false;
                }

                if ((scrollX | scrollY) == 0) {
                    background.draw(canvas);
                } else {
                    canvas.translate(scrollX, scrollY);
                    background.draw(canvas);
                    canvas.translate(-scrollX, -scrollY);
                }
            }
        }

このコードは背景図を描くもので、コードには特別なところはありません.注意が必要だsetBounds(0, 0,  mRight - mLeft, mBottom - mTop);ここではバックグラウンドのサイズを設定します.つまり、mBackgroundSizeChangedフラグ量がtrueの場合、そのboundが設定されます.ソースコードを見てもいいですが、mBackgroundSizeChangedはsetFrameメソッドで呼び出され、setFrame()はlayout()で呼び出されます.はっきり言って、Viewが再びlayoutを再設定すると、バックグラウンドのサイズが再設定されます.もちろん、最初のケンケンは設定が必要で、mBackgroundSizeChangedが呼び出されると#mBackgroundSizeChanged()がtrueに設定されます.
// skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);

            // we're done...
            return;
        }
このコードは、上述した第2および第5のステップをスキップすることである.垂直エッジを描画する必要もなく、水平エッジを描画する必要もない場合は、3、4、6ステップの通常の論理を実行します.
ステップ3:自分の内容を描き、if(!dirtyOpaque)onDraw(canvas);
ステップ4:子供を描く、dispatchDraw(canvas);
ステップ6では、スクロールバー、onDrawScrolBars(canvas);
この数歩で、描画が完了し、直接returnします.
第5歩の実現について、この一歩、コードは多くて、その核心はcanvasのlayersを保存して、更に描いて、更にそのlayersを復元して、コードは私は貼らないで、興味があるのはソースコードを見ることができます.
二、View Groupはどのようにchildを描きますか
第1節から分かるように、Viewの中の実装ペイントの子供はView#dispatchDrawを呼び出し、View Groupはこの方法を実現し、一定のアルゴリズムに従ってchildを描く.dispatchDrawの実装を見てみましょう.
if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE;

            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, count);
                    bindLayoutAnimation(child);
                    if (cache) {
                        child.setDrawingCacheEnabled(true);
                        child.buildDrawingCache(true);
                    }
                }
            }

            final LayoutAnimationController controller = mLayoutAnimationController;
            if (controller.willOverlap()) {
                mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE;
            }

            controller.start();

            mGroupFlags &= ~FLAG_RUN_ANIMATION;
            mGroupFlags &= ~FLAG_ANIMATION_DONE;

            if (cache) {
                mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE;
            }

            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }
        }

これらはすべて終わって、もうすぐそのchildを描き始めます.まず、このコードを見てください.
if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        }

これ(flags&FLAG_USE_CHILD_DRAWING_ORDER)==0現在このViewGroupがdrawing orderを使用しているかどうかをチェックすることです.このdrawing orderはどういう意味ですか.デフォルトでは、後に追加(後にaddViewを呼び出す)するchildは、通常は最後に描画されます.後に追加されるので、一番上に表示されるはずです.しかし、gallery、listview、gridviewのような特殊なView Groupでは、このような方法で描画を管理することはできません.gallery、listview、gridviewのchildが多重化される可能性があります.最初に追加されるのは、最後のデータが表示される可能性があります.そのため、このとき彼は一番上に表示する必要があります.
上のコード、if((flags&FLAG_USE_CHILD_DRAWING_ORDER)==0)が成立していれば、つまり現在のViewGroupでdrawing orderを使用する必要がなく、通常の0-countからchildを描画します.ここではViewGroup#drawChildを呼び出し、その分析は後述する.上記の条件が成立しない場合はdrawing orderで描画され、ViewGroup#getChildDrawingOrder()メソッドを呼び出してインデックス値を返します.
最後に、アニメーションが終了すると、ViewGroup#ontifyAnimationListener()メソッドが呼び出されます.
if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 &&
                mLayoutAnimationController.isDone() && !more) {
            // We want to erase the drawing cache and notify the listener after the
            // next frame is drawn because one extra invalidate() is caused by
            // drawChild() after the animation is over
            mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER;
            final Runnable end = new Runnable() {
               public void run() {
                   notifyAnimationListener();
               }
            };
            post(end);
        }
このように、dispatchDrawは終わっても、実は彼の論理は複雑ではありません.最も本質的には、drawChildなどの方法を呼び出してchildを描き、flagを設定することです.
ViewGroup#drawChildの実装については、最も本質的に次の2点を処理する必要があります.
1,アニメーション,アニメーションから変換マトリクスを取り出し,このマトリクスに基づいてchildを描画する.
2,Alpha値,AlphaAnimationが存在するためcanvasにalpha値を設定する必要がある.
コードを見ると、いくつかのコアコードがあります.
final Animation a = child.getAnimation();
more = a.getTransformation(drawingTime, mChildTransformation);
ここでは、まずアニメーションのオブジェクト(ある場合)を取り、次に、マトリクス情報とAlpha値を含むアニメーションの現在の時間に対応する変換(Transformation)を取りに行きます.このView Group#drawChild()メソッドでは、View#onAnimationStart()メソッドを呼び出したり、View#draw()メソッドを呼び出してchildを描画したりします.
三、invalidate
invalidateについては、現在のviewを無効にマークし、ペイントメッセージを送信し、このメッセージがペイントをトリガーすることを理解しています.したがって、UIを更新するには、この方法を呼び出します.簡単に言えば、UIを再描画するにはinvalidate()方法を呼び出します.
/**
     * Invalidate the whole view. If the view is visible, {@link #onDraw} will
     * be called at some point in the future. This must be called from a
     * UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.
     */
    public void invalidate() {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
        }

        if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
            mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
            final ViewParent p = mParent;
            final AttachInfo ai = mAttachInfo;
            if (p != null && ai != null) {
                final Rect r = ai.mTmpInvalRect;
                r.set(0, 0, mRight - mLeft, mBottom - mTop);
                // Don't call invalidate -- we don't want to internally scroll
                // our own bounds
                p.invalidateChild(this, r);
            }
        }
    }
では、parentのViewParent#invalidateChild()メソッドが呼び出され、ViewGroupがこのメソッドを実装します.
/**
     * Don't call or override this method. It is used for the implementation of
     * the view hierarchy.
     */
    public final void invalidateChild(View child, final Rect dirty) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE_CHILD);
        }

        ViewParent parent = this;

        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            final int[] location = attachInfo.mInvalidateChildLocation;
            location[CHILD_LEFT_INDEX] = child.mLeft;
            location[CHILD_TOP_INDEX] = child.mTop;

            // If the child is drawing an animation, we want to copy this flag onto
            // ourselves and the parent to make sure the invalidate request goes
            // through
            final boolean drawAnimation = (child.mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION;

            // Check whether the child that requests the invalidate is fully opaque
            final boolean isOpaque = child.isOpaque() && !drawAnimation &&
                    child.getAnimation() != null;
            // Mark the child as dirty, using the appropriate flag
            // Make sure we do not set both flags at the same time
            final int opaqueFlag = isOpaque ? DIRTY_OPAQUE : DIRTY;

            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }

                if (drawAnimation) {
                    if (view != null) {
                        view.mPrivateFlags |= DRAW_ANIMATION;
                    } else if (parent instanceof ViewRoot) {
                        ((ViewRoot) 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 && (view.mPrivateFlags & DIRTY_MASK) != DIRTY) {
                    view.mPrivateFlags = (view.mPrivateFlags & ~DIRTY_MASK) | opaqueFlag;
                }

                parent = parent.invalidateChildInParent(location, dirty);
            } while (parent != null);
        }
    }

ViewGroup#invalidateChild()メソッドにはdo whileループがあります.それは何をしているのでしょうか.簡単に言えば、dirtyの領域とlocationを特定します.このdirtyのRectとint[]のlocationはAttachInfoのメンバー変数です.つまり、do whileループを実行した後、AttachInfoのdirty Rectは計算され、dirty領域があります.下部には、指定した領域を描画できます.
ここでのdo-whileループは、現在のview(invalidateを呼び出すそのView)からそのparentを1層ずつ上に探し、parentのViewParent#invalidateChildInParent()をルートViewが見つかるまで呼び出す(Root viewにはparentがない).
では、なぜdirty領域を計算するのでしょうか.View#invalidate()メソッドを呼び出すと、デフォルトの領域はView全体のサイズであり、この領域のサイズはparentの領域と交差する可能性があり、parentのparentの領域と交差する可能性があるため、最終的に描画する必要がある領域のサイズを決定するには、階層的に上を向く必要があることがわかります.
ViewParent#invalidateChildInParentメソッドの実装は、次のとおりです.
/**
     * Don't call or override this method. It is used for the implementation of
     * the view hierarchy.
     *
     * This implementation returns null if this ViewGroup does not have a parent,
     * if this ViewGroup is already fully invalidated or if the dirty rectangle
     * does not intersect with this ViewGroup's bounds.
     */
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE_CHILD_IN_PARENT);
        }

        if ((mPrivateFlags & DRAWN) == DRAWN) {
            if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) !=
                        FLAG_OPTIMIZE_INVALIDATE) {
                dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                        location[CHILD_TOP_INDEX] - mScrollY);

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

                if (dirty.intersect(0, 0, mRight - left, mBottom - top) ||
                        (mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION) {
                    mPrivateFlags &= ~DRAWING_CACHE_VALID;

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

                    return mParent;
                }
            } else {
                mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;

                location[CHILD_LEFT_INDEX] = mLeft;
                location[CHILD_TOP_INDEX] = mTop;

                dirty.set(0, 0, mRight - location[CHILD_LEFT_INDEX],
                        mBottom - location[CHILD_TOP_INDEX]);

                return mParent;
            }
        }

        return null;
    }
上のコードから分かるように、それは常に
mParent、つまり、自分のparentに戻ります.この方法では,主にdirtyとlocationの値を計算するが,前述したように,この2つの値はattachInfoに存在する.AttachInfoはViewの内部クラスで、現在のViewとWindowの一連の情報、例えばWindow、Handler、RootView、そしてさっき言ったdirty rect(mTmpInvalRect)、location(mInvalidateChildLocation)です.
このattachInfoはいつビューに設定されたのですか?このとき、Activityが作成されるとsetContentViewメソッドが呼び出され、ソースコードの実装を見ることができます.
public void setContentView(View view) {
        getWindow().setContentView(view);
    }
それは現在のwindowを得て、setContentViewを呼び出して、もしあなたが興味があれば研究を続けることができて、最終的なコードは
frameworksbasepolicysrccomandroidinternalpolicyimplPhoneWindowで実装されています.
ViewdispatchAttachedToWindowメソッドでは、attachInfoが設定されます.ViewGroupはこの方法を書き直しました.その中にはchildを順番に呼び出しています.
dispatchAttachedToWindowメソッド.
このメソッドはView Rootでも呼び出されます.
以上、Androidの描画メカニズムに関する研究ですが、浅いかもしれませんが、深く研究する必要がある問題もあります.