Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android Scroll詳解(三):Android 繪制過程詳解

Android Scroll詳解(三):Android 繪制過程詳解

編輯:關於Android編程

本文轉載請注明原作者、文章來源,鏈接,版權歸原文作者所有。

本篇為Android Scroll系列文章的最後一篇,主要講解Android視圖繪制機制,由於本系列文章內容都是視圖滾動相關的,所以,本篇從視圖內容滾動的視角來梳理視圖繪制過程。
如果沒有看過本系列之前文章或者不太了解相關的知識,請大家閱讀一下一下的文章:


mScrollY是如何影響視圖內容。 Android視圖繪制邏輯,包括相關API和Canvas的相關操作。?為了節約大家的時間,本文內容主要如下:

Scroller相關機制。 mScrollX

一切從Scroller使用開始

?使用scroller的實例代碼,之後的講解流程就是scroller和computeScroll是如何調用的啦。
?在系列文章的第二篇中,我們具體學習了Scroller的使用方法。通過ScrollerflingViewcomputeScroll的配合,實現視圖滾動效果。實例代碼如下

.....       
    mScroller.fling(0,getScrollY(),0,speed,0,0,-500,10000)
    invalidate();

    .....
    @Override
    public void computeScroll() {
            if (mScroller.computeScrollOffset()) {        
                scrollTo(mScroller.getCurrX(),
                        mScroller.getCurrY());
               postInvalidate();
            }
    }

?本篇文章就帶大家探究一下這段代碼背後的原理和機制。

Invalidate的尋父之路

?這一節主要分析在View中調用invalidateViewRoot執行performTraversals的原理,對android視圖架構不是很熟悉的同學可以先閱讀一下《Android視圖架構詳解》。
validate時序圖

?我們先來看一下View中的invalidate代碼。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> public void invalidate() { invalidate(true); } void invalidate(boolean invalidateCache) { invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true); } void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) { ..... //DRAWN和HAS_BOUNDS是否被設置為1,說明上一次請求執行的UI繪制已經完成,那麼可以再次請求執行 if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS) || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID) || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED || (fullInvalidate && isOpaque() != mLastIsOpaque)) { if (fullInvalidate) { mLastIsOpaque = isOpaque(); mPrivateFlags &= ~PFLAG_DRAWN; } mPrivateFlags |= PFLAG_DIRTY; if (invalidateCache) { //是否讓view的緩存都失效 mPrivateFlags |= PFLAG_INVALIDATED; mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID; } // Propagate the damage rectangle to the parent view. final AttachInfo ai = mAttachInfo; final ViewParent p = mParent; //通過ViewParent來執行操作,如果當前視圖是頂層視圖也就是DecorView的視圖,那麼它的 //mParent就是ViewRoot對象,所以是通過ViewRoot的對象來實現的。 if (p != null && ai != null && l < r && t < b) { final Rect damage = ai.mTmpInvalRect; damage.set(l, t, r, b); p.invalidateChild(this, damage);//TODO:這是invalidate執行的主體 } ..... } }

?我們可以看到,調用invalidate()會導致整個視圖進行刷新,並且會刷新緩存。
?然後我們再來詳細的研究一下invalidateInternal中的代碼。我們先來著重看一下if語句的判斷條件把。


    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
            || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
            || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
            || (fullInvalidate && isOpaque() != mLastIsOpaque))
mPrivateFlagsFLAG_DRAWNFLAG_HAS_BOUNDS位設置為1時,說明上一次請求執行的UI繪制已經完成,那麼可以再次請求重新繪制。FLAG_DRAWN位會在draw函數中會被置為1,而FLAG_HAS_BOUNDS會在setFrame函數中被設置為1。 mPrivateFlagsPFLAG_DRAWING_CACHE_VALID標示視圖緩存是否有效,如果有效並且invalidateCache為true,那麼可以請求重新繪制。 另外兩個布爾判斷的具體含義並沒有分析清楚,大家感興趣的請自行研究。
?然後將mPrivateFlagsPFLAG_DIRTY置為1。並且如果是要刷新緩存的話,將PFLAG_INVALIDATED位設置為1,並且將PFLAG_DRAWING_CACHE_VALID位設置為0,這一步和之前的if判斷中後兩個布爾判斷相對應,可見,如果已經有一個invalidate設置了上述兩個標志位,那麼下一個invalidate就不會進行任何操作。
?接著,調用ViewParent接口的invalidateChild函數,在《Android視圖架構詳解》,我們已經知道ViewGroupViewRoot都實現了上述接口,那麼,根據Android視圖樹狀結構,ViewGroup的相應方法會被調用。

public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        ....
        // while一直向上遞歸
        do {
            ......
            parent = parent.invalidateChildInParent(location, dirty);
            ....
        } while (parent != null);
    }
}
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) {
            ......
            return mParent;

        } else {
            .....
            return mParent;
        }
    }
    return null;
}

?通過上述代碼我們可以看到ViewGroupinvalidateChild函數通過循環不斷調用其父視圖的invalidateChildInParent,而且我們知道ViewRootDecorView的父視圖,也就是說ViewRoot是Android視圖樹狀結構的根。所以,最終ViewRootinvalidateChildInParent會被調用。



  //在ViewGroup的invalidateChildInParent中while循環,一直調用到這裡,然後在調用invalidateChild
    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        invalidateChild(null, dirty);
        return null;
 }
 public void invalidateChild(View child, Rect dirty) {
    //先檢查線程,必須是主線程
    checkThread();
    .....
    //如果mWillDrawSoon為true那麼就是消息隊列中已經有一個DO_TRAVERSAL的消息啦
    if (!mWillDrawSoon) {
         //直接調用了這個喽
        scheduleTraversals();
    }
}

?最終,在ViewRootinvalidateChild函數中,調用了scheduleTraversals,開啟了視圖重繪之旅。

我們都被ViewRoot騙了

?ViewRoot是Android視圖樹狀結構的根節點,並且它實現了ViewParent接口,是DecorView的父視圖。那麼大家一定會認為它就是一個View吧。那我們就被它給騙了!!ViewRoot本質上是一個Handler,我們可以看一下scheduleTraversalsperformTraversals的原理就知道了。


public void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        sendEmptyMessage(DO_TRAVERSAL);
    }
}

?在scheduleTraversals中,ViewRoot只是向自己發送了一個DO_TRAVERSAL的空信息。


    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
        ....
        case DO_TRAVERSAL:
        //這裡就是Handle處理travel信息的地方
            if (mProfile) {
                Debug.startMethodTracing("ViewRoot");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
            break;
            .....
        }
    }

?然後我們在查看handleMessage方法,發現在處理DO_TRAVERSAL時,ViewRoot調用了performTraversals函數。
?在performTraversals中,視圖要進行measure,layout,和draw三大步驟,篇幅有限,我們這裡只研究繪制相關的機制。
?ViewRootperformTraversals中調用了自身的draw方法,看吧,ViewRoot偽裝的還挺像,連draw方法都有。但是我們會發現,在draw方法中,ViewRoot實際上只調用了自己的mView成員變量的draw方法,而且我們都知道的是,mView就是DecorView,於是,繪制流程來到了真正的View視圖的根節點。

大家都來畫的canvas

?接下來,我們就正式研究一下Android的繪制機制,我們沿著Android視圖的樹狀結構來分析繪制原理。
draw時序圖

?首先是DecorView的繪制相關的函數。在ViewRootdraw方法中,直接調用了DecorViewdraw(Canvas canvas)函數,我們知道DecorViewFrameLayout的子類,其draw(Canvas canvas)函數是從View中繼承而來的。所以我們先來看Viewdraw(Canvas canvas)方法。


    // http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/view/View.java#View
    public void draw(Canvas canvas) {
             ........
            /*
             * Draw traversal performs several drawing steps which must be executed
             * in the appropriate order:
             *
             *      1. Draw the background
             *      2. If necessary, save the canvas' layers to prepare for fading
             *      3. Draw view's content
             *      4. Draw children
             *      5. If necessary, draw the fading edges and restore layers
             *      6. Draw decorations (scrollbars for instance)
             */

            // Step 1, draw the background, if needed
            if (!dirtyOpaque) {
                drawBackground(canvas);
            }
            .......

            // Step 2, save the canvas' layers
            .......
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

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

            // Step 5, draw the fade effect and restore layers
            .......
            if (drawTop) {
                matrix.setScale(1, fadeHeight * topFadeStrength);
                matrix.postTranslate(left, top);
                fade.setLocalMatrix(matrix);
                p.setShader(fade);
                canvas.drawRect(left, top, right, top + length, p);
            }
            .....
            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);
            ......
        }

?關於視圖的組成部分,我在之前的文章中已經講述過來,請不太熟悉這部分內容的同學自行查閱文章或者其他資料。通過上述代碼我們可以看到,ViewdispatchDraw函數被調用了,它是向子視圖分發繪制指令和相關數據的方法。在View中,上述函數是一個空函數,但是ViewGroup中對這個函數進行了實現。


    protected void dispatchDraw(Canvas canvas) {
        ....
        final ArrayList preorderedList = usingRenderNodeProperties
                ? null : buildOrderedChildList();
        final boolean customOrder = preorderedList == null
                && isChildrenDrawingOrderEnabled();
        for (int i = 0; i < childrenCount; i++) {
            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) {
                //在這裡drawChild
                more |= drawChild(canvas, child, drawingTime);
            }
        }
        ....
    }
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    //這裡就調用child的draw方法啦,而不是draw(canvas)方法!!!!!
        return child.draw(canvas, this, drawingTime);
    }

?通過上述代碼我們可以看到,ViewGroup分別調用了自己的子View的draw方法,需要特別注意的是,這個draw和之前draw方法不是同一個方法,他們的參數不同。於是,我們再次轉到View的源碼中,看一下這個draw方法到底做了什麼。


    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ....
        //進行計算滾動
        if (!hasDisplayList) {
            computeScroll();
            sx = mScrollX;
            sy = mScrollY;
        }
        ...
        //這裡進行了平移。
        if (offsetForScroll) {
            canvas.translate(mLeft - sx, mTop - sy);
        }
        ..... 
        if (!layerRendered) {
          if (!hasDisplayList) {
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
              mPrivateFlags &= ~PFLAG_DIRTY_MASK;
              dispatchDraw(canvas);
            } else {
              // 在這裡調用了draw
              draw(canvas);
            }
          } 
                ......
        }
        ......
    }

?首先,我們發現computeScroll方法是在其中被調用的,從而計算出新的mScrollXmScrollY,然後在平移畫布,產生內容平移效果。
?然後我們發現通過PFLAG_SKIP_DRAW標志位的判斷,有些View是直接調用dispatchDraw函數,說明它自己沒有需要繪制的內容,而有些View則是調用自己的draw方法。我們應該都知道ViewGroup默認是不進行繪制內容的吧,我們一般調用setNotWillDraw方法來讓其可以繪制自身內容,通過調用setNotWillDraw方法,會導致PFLAG_SKIP_DRAW位被置為1,從而可以繪制自身內容。
?分析到這裡,我們就會發現draw函數沿著Android視圖樹狀結構被不斷調用,知道所有視圖都完成繪制。

把一切連接起來的computeScroll

?讀到這裡大家應該對Android視圖繪制流程有了基本的了解了吧,那麼,我們再來看一下文章開頭的例子。在computeScroll方法中,我們調用了postInvalidate方法,這又是什麼用意呢?
?其實,在computeScroll中不掉用postInvalidate好像也可以達到正確的效果,具體原因我不太了解,猜測應該是Android自動刷新界面可以代替postInvalidate的效果吧。同學們如果知道其中具體原因,請告知我啊。
?在《Android Scroll詳解(一):基礎知識》中,我們已經講到
postInvalidate其實就是調用了invalidate,然後整個流程就連接了起來,mScrollXmScrollY每個循環都會改變一點,然後導致界面滾動,最終形成界面Scroll效果。

後記

?Android Scroll的系列文章就此結束了,希望大家從中學習到有用的知識。如果其中有任何錯誤或者容易誤解的地方,請大家及時通知我。謝謝各位讀者和同學。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved