Android中的繪製機制

來源:互聯網
上載者:User

我們知道,其實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);                }            }        }

這段代碼就是繪製背景圖了,代碼沒有什麼特別之處。這裡有一點需要注意,background.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步,繪製捲軸,onDrawScrollBars(canvas);

經過這幾步,繪製就完成了,直接return。

關於第5步的實現,這一步,代碼很多,其核心就是儲存canvas的layers,再繪製,再還原其layers,代碼我就不貼出來了,有興趣的可以去看源碼。

二,ViewGroup如何繪製child

從第一節我們可以得知,View裡面的實現繪製子孩子是調用了View#dispatchDraw,ViewGroup會實現這個方法,去按照一定的演算法去繪製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 判斷就是去check當前這個ViewGroup是否使用drawing order,這個drawing order是什麼意思呢?預設情況下,後加入(後調用addView)的child,通常是最後繪製,因為它後加入,理應顯示在最上面。但是,還有一種情況,gallery,listview,gridview這樣的特殊的ViewGroup,你就不能按照這種方式去管理繪製,因為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的實現,最本質上需要處理以下兩點:

    1,動畫,從動畫中取出一個變換矩陣,根據這個矩陣去繪製出child。

    2,Alpha值,因為存在AlphaAnimation,所以,需要給canvas設定alpha值。

我們看代碼會發現,有這麼幾句核心代碼:

final Animation a = child.getAnimation();

more = a.getTransformation(drawingTime, mChildTransformation);

這裡,先去拿動畫的對象(如果有的話),然後,去取到動畫中目前時間所對應的變換(Transformation),這裡麵包含了矩陣資訊和Alpha值。這ViewGroup#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,然後調用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的值,前面已經說過了,這兩個值都存在attachInfo裡面。AttachInfo是View的一個內部類,它就是當前View和Window的一系列資訊,如Window, Handler, RootView,還有剛才說的dirty rect (mTmpInvalRect), location (mInvalidateChildLocation)。

這個attachInfo是何時設定到View裡面的呢?此時,我們想一想,在Activity建立的時候,我們會調用setContentView方法,你可以看其源碼的實現:

public void setContentView(View view) {        getWindow().setContentView(view);    }

它得到當前的window,調用setContentView,如果你有興趣可以繼續研究,最終代碼是在frameworks\base\policy\src\com\android\internal\policy\impl\PhoneWindow中實現的。View#dispatchAttachedToWindow方法,會設定一個attachInfo。ViewGroup重寫了這個方法,它裡面,也就是依次調用child的dispatchAttachedToWindow方法。這個方法還會在ViewRoot中調用。

以上就是我關於Android中的繪製機制的一些研究,可能很膚淺,有些問題還需要深入研究。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.