經過抽象的方式來說一講View的繪製流程

前言: 不少時候,咱們在看源碼時,看的時候能夠理解其原理,可是事後不久又容易忘記,這是由於沒有留下一個印象,知道本身看了啥,可是又感受說不出來,這就是沒有概括總結致使的緣由;
那麼當前已經有不少人寫了View 的繪製流程,這裏會經過圖文的方式來進行總結!但願對你有所幫助;html

視覺效果

在開始以前,咱們先來看一張圖片:git

很熟悉的淘寶首頁,而Android的大部分界面都是圖形界面,這些圖形究竟是怎麼來的呢?系統是怎麼繪製出來的呢?讓咱們帶着思考繼續看下去;github

View的層級關係

1,首先,先來看一下View的層級關係,View的最頂層是Activity,Activity裏面是PhoneWindow,而PhoneWindow裏面則是最頂層的View(DecorView),DecorView裏面包含的就是咱們肉眼能夠看到的圖形界面了,也就是上面的淘寶首頁,而繪製的起點也正是從DecorView開始的;canvas

從上面的圖能夠清楚的看出各個層級的關係,到了DecorView這一層,就是咱們最熟悉的View樹結構了;bash

那麼到了這裏,又會有一個疑問了,DrcorView裏面是怎麼將View樹繪製出來的呢?app

別急,且聽我細細道來;less

下面咱們先來看一張圖:源碼分析

這張圖詳細代表了DecorView添加到Window的過程,這裏面看到了一個很熟悉的方法,requestLayout(),這個方式是在ViewRootIml裏面調用的,來看看官方API的解釋:佈局

Called when something has changed which has invalidated the layout of a child of this view parent.post

這句話什麼意思呢? 翻譯過來的意思就是調用這個方法會致使當前視圖的子View佈局失效,也就是說調用這個方法會致使View樹的從新layout;

ViewRootIml

在DecorView調用了requestLayout()方法以後,最終會走到View的繪製流程,前面流程的源碼我這裏就不貼出來了,建議看完本身跟着源碼走一遍;

最終的繪製流程是在performTraversals()方法裏面;

private void performTraversals() {
    // Ask host how big it wants to be, 執行測量操做
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    
    //執行佈局操做
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    
    //執行繪製操做
    performDraw();
}
複製代碼

performTraversals()裏面的源碼很長很複雜,這裏咱們只須要關注測量,佈局,繪製這幾個方法便可;

看一下視圖繪製的流程圖:

讓咱們回憶一下以前的問題,答案如今已經很明顯了,就是系統調用ViewRootIml類裏的performTraversals()去繪製整個界面的;

什麼?這就沒了???

這位大俠,請放下你手中的刀,我還沒講完呢!

前面已經把View繪製流程的入口已經理清楚了,那麼接下來就繼續分析performTraversals()裏的調用吧;

測量

先來看一下performMeasure()方法的源碼:

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        if (mView == null) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
複製代碼

這一段源碼很簡單,就是調用了View的mesure方法,這個mView就是最頂層的DecorView,這裏傳了(childWidthMeasureSpec, childHeightMeasureSpec)這兩個參數,那麼這兩個參數是什麼意思呢?有什麼用呢?請繼續往下看;

測量規格

在調用performMeasure()方法以前,會先調用getRootMeasureSpec()方法來獲取測量規格,也就是childWidthMeasureSpec, childHeightMeasureSpec這兩個參數;

看一下getRootMeasureSpec()方法的源碼:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window cant resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
複製代碼

在繼續深刻分析以前,先來看一下測量模式的類型:

1.MeasureSpec.EXACTLY:肯定模式,父容器但願子視圖View的大小是固定,也就是specSize大小。這裏能夠理解爲有具體的大小,好比MATCH_PARENT或者10dp這種;

2.MeasureSpec.AT_MOST:最大模式,父容器但願子視圖View的大小不超過父容器但願的大小,也就是不超過specSize大小。這裏理解爲沒有固定的大小,由子類去計算,對應WRAP_CONTENT這種;

3.MeasureSpec.UNSPECIFIED: 不肯定模式,子視圖View請求多大就是多大,父容器不限制其大小範圍,也就是size大小。這種能夠理解爲沒有對子View添加束縛,好比列表控件,RecyclerView,ListView這種;

接下來再回到getRootMeasureSpec()這個方法中,源碼根據傳進來的寬高來獲取測量的規格;

第一個case爲ViewGroup.LayoutParams.MATCH_PARENT時:
使用了MeasureSpec.EXACTLY的測量模式,也就是有具體的大小;

第二個case爲ViewGroup.LayoutParams.WRAP_CONTENT時:
使用了MeasureSpec.AT_MOST的測量模式,也就是沒有具體的大小;

第三個case爲MeasureSpec.UNSPECIFIED:
和第一個相同;

DecorView默認的寬高爲MATCH_PARENT,那麼這裏就會走第一個case去獲取測量規格,也就是說最頂層的測量規格就是從這裏獲取的;

View的測量

到這裏,測量規格弄清楚了,接下來分析View的measure()方法;

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        // 判斷是否須要強制佈局,也就是會觸發從新測量
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        // 將當前的規格和上一次測量的規格作比較,判斷是否須要從新測量
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        if (forceLayout || needsLayout) {
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
    }
複製代碼

這裏主要判斷是否須要從新測量,若是須要則調用onMeasure()去測量;

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製代碼

onMeasure()方法很簡單就幾行代碼,若是子類沒有重寫這個方法去測量寬高,則使用默認的方法getDefaultSize()去獲取寬高,而後再調用setMeasuredDimension()去設置View的寬高;

看一下getDefaultSize()這個方法的源碼:

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
複製代碼

經過寬高size和測量的規格measureSpec來計算最終的寬高,也就是說若是佈局裏面的子View沒有從新onMeasure()時,則會使用默認的方法來獲取寬高,那麼佈局使用測量模式爲MeasureSpec.EXACTLY和MeasureSpec.AT_MOST時,寬高都是返回由測量模式和具體大小計算以後的值specSize;

那麼到這裏測量的方法差很少就分析完了,可是還有一個疑問,也就是View樹是怎麼測量的呢?

接下來繼續分析;

前面分析的是子View沒有從新onMeasure()的狀況,接下來分析子View重寫了onMeasure()的狀況;

LinearLayout的測量

舉個熟悉的例子,LinearLayout控件是咱們最經常使用的ViewGroup控件,下面以這個爲例子來進行分析;

看一下LinearLayout控件的onMeasure()方法:

rotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }
複製代碼

再看一下measureVertical的方法:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
   
        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);\
            ...
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                        heightMeasureSpec, usedHeight);


        }
        ...
        // Check against our minimum width
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);
    }
複製代碼

這源碼裏面經過遍歷當前的子View,而後經過measureChildBeforeLayout()去測量子View的寬高,並經過計算子View的寬高來調用setMeasuredDimension()設置LinearLayout的寬高,而測量子View 的方法裏面最終調用的是ViewGroup的measureChildWithMargins()方法;

void measureChildBeforeLayout(View child, int childIndex,
            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
            int totalHeight) {
        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
                heightMeasureSpec, totalHeight);
    }
複製代碼

ViewGroup裏的方法最終調用了View裏的measure方法,而ViewGroup裏面也自定義了獲取測量模式的方法getChildMeasureSpec(); 這裏細節就不過多關注了;

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
複製代碼

那麼到這裏,ViewRootIml的performMeasure()方法的流程就能夠總結爲下面這張圖:

整個View樹的測量流程就是經過這種遞歸的方式,一步步的測量完成的;

總結:
1,View樹的測量是經過遞歸的方式測量完成的,遞歸的方法爲View的measure()方法;
2,View和ViewGroup都有本身的測量模式的方法,固然子View也能夠自定義獲取測量模式的方法;
3,View樹測量結束以後,會調用setMeasuredDimension()讓以前測量的寬高設置生效,這個方法是在遞歸結束以後,經過View樹的最底層往上傳遞的;
4,子View的大小是由父視圖和子視圖共同決定的;

測量的流程已經講完了,接下來開始講佈局的流程,既然測量的流程是經過遞歸的方式,那麼佈局的流程是否是也?

是的,沒錯,也是經過遞歸的方式;

別急,且請我細細道來!

佈局

對View進行佈局的目的是計算出View的尺寸以及在其父控件中的位置,具體來講就是計算出View的四條邊界分別到其父控件左邊界、上邊界的距離,即計算View的left、top、right、bottom的值。

先來看一下performLayout()方法的源碼:

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
       // 標記佈局開始
        mInLayout = true;

        final View host = mView;
        ...
        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            ...
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        // 標記佈局結束
        mInLayout = false;
    }
複製代碼

這裏調用了mView的layout()方法,這個mView就是最頂層的DecorView,而layout()方法則爲View裏的方法;

View的佈局

看看View的layout()方法裏面作了啥?

public void layout(int l, int t, int r, int b) {
       ...

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        //若是isLayoutModeOptical()返回true,那麼就會執行setOpticalFrame()方法,
        //不然會執行setFrame()方法。而且setOpticalFrame()內部會調用setFrame(),
        //因此不管如何都會執行setFrame()方法。
        //setFrame()方法會將View新的left、top、right、bottom存儲到View的成員變量中
        //而且返回一個boolean值,若是返回true表示View的位置或尺寸發生了變化,
        //不然表示未發生變化
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //若是View的佈局發生了變化,或者mPrivateFlags有須要LAYOUT的標籤PFLAG_LAYOUT_REQUIRED,
            //那麼就會執行如下代碼
            //首先會觸發onLayout方法的執行,View中默認的onLayout方法是個空方法
            //不過繼承自ViewGroup的類都須要實現onLayout方法,從而在onLayout方法中依次循環子View,
            //並調用子View的layout方法
            onLayout(changed, l, t, r, b);
            ...
        }

        ...
    }
複製代碼

這裏只須要關注setFrame()方法和onLayout()方法便可,onLayout()方法由子類實現,先來看一下setFrame()方法;

protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            //將新舊left、right、top、bottom進行對比,只要不徹底相對就說明View的佈局發生了變化,
            //則將changed變量設置爲true
            changed = true;
            ...
            // 分別計算View的新舊尺寸
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            // 比較View的新舊尺寸是否相同,若是尺寸發生了變化,那麼sizeChanged的值爲true
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            ...

            // 將新的left、top、right、bottom存儲到View的成員變量中
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            ...
            //若是View的尺寸和以前相比發生了變化,那麼就執行sizeChange()方法,
            //該方法中又會調用onSizeChanged()方法,並將View的新舊尺寸傳遞進去
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            ...
        }
        return changed;
    }
複製代碼

在該方法中,會將新舊left、right、top、bottom進行對比,只要不徹底相同就說明View的佈局發生了變化,則將changed變量設置爲true。而後比較View的新舊尺寸是否相同,若是尺寸發生了變化,並將其保存到變量sizeChanged中。若是尺寸發生了變化,那麼sizeChanged的值爲true。

而後將新的left、top、right、bottom存儲到View的成員變量中保存下來。並執行mRenderNode.setLeftTopRightBottom()方法會,其會調用RenderNode中原生方法的nSetLeftTopRightBottom()方法,該方法會根據left、top、right、bottom更新用於渲染的顯示列表。

而onLayout()方法由子類實現,若是子類是View的話,則方法不須要實現,若是是ViewGroup的話,由於方法爲抽象方法,那麼必須由子類實現;這裏經過LinearLayout的onLayout()方法來進行舉例說明;

LinearLayout的佈局

看一下LinearLayout的onLayout()方法:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
複製代碼

查看其中一種方法layoutVertical();

void layoutVertical(int left, int top, int right, int bottom) {
        ...

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
                
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                ...
            }
        }
    }
複製代碼

經過遍歷全部的子View,調用setChildFrame()進行佈局,再看一下setChildFrame()方法的源碼;

private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }
複製代碼

這裏調用了View的layout()的方法來進行子View的layout;

到這裏,佈局的流程就分析完了,看一下流程圖:

總結:
1,View樹的佈局是經過遞歸的方式測量完成的,遞歸的方法爲View的layout()方法;
2,View和ViewGroup都有onLayout()方法,可是ViewGroup的方法是抽象的,必須由子類實現,View的佈局是由ViewGroup來控制的,也就是說View並不須要進行onLayout();
3,使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程以後被調用才能返回有效值;

那麼到這裏,佈局的流程就已經講完了,接下來分析繪製的流程;

既然測量,和佈局都是用遞歸的方式,那繪製豈不是也?

是的,繼續往下看,理解了一個以後,其餘理解起來也不難!

繪製

最後一步的繪製會將頁面展現在咱們面前,前面的操做都只是準備工做;

先來看一下performDraw()的源碼:

private void performDraw() {
        ...
        try {
            boolean canUseAsync = draw(fullRedrawNeeded);
            ...
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }
複製代碼

這裏調用了ViewRootIml裏面的draw()方法,跟蹤源碼發現最終調用的是drawSoftware()方法;

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty, Rect surfaceInsets) {

        
        try {
            // 從surface裏面獲取canvas對象
            canvas = mSurface.lockCanvas(dirty);

            ...
        } catch (Surface.OutOfResourcesException e) {
            handleOutOfResourcesException(e);
            return false;
        } catch (IllegalArgumentException e) {
            ...
            return false;
        } finally {
            dirty.offset(dirtyXOffset, dirtyYOffset);  // Reset to the original value.
        }

        try {
            if (DEBUG_ORIENTATION || DEBUG_DRAW) {
                Log.v(mTag, "Surface " + surface + " drawing to bitmap w="
                        + canvas.getWidth() + ", h=" + canvas.getHeight());
                //canvas.drawARGB(255, 255, 0, 0);
            }

            ...
            try {
                ...
                // 調用View的draw()方法
                mView.draw(canvas);

            } finally {
                ...
            }
        } finally {
           ...
        }
        return true;
    }
複製代碼

drawSoftware()方法裏面先從mSurface獲取canvas對象,而後經過mView調用draw()方法時,將canvas做爲參數傳進去;最後調用的是View的draw()方法;

View的繪製

接下來分析一下View的draw()方法;

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
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // 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);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we're done... return; } /* * Here we do the full fledged routine... * (this is an uncommon case where speed matters less, * this is why we repeat some of the tests that have been * done above) */ boolean drawTop = false; boolean drawBottom = false; boolean drawLeft = false; boolean drawRight = false; float topFadeStrength = 0.0f; float bottomFadeStrength = 0.0f; float leftFadeStrength = 0.0f; float rightFadeStrength = 0.0f; // Step 2, save the canvas' layers
        int paddingLeft = mPaddingLeft;

        final boolean offsetRequired = isPaddingOffsetRequired();
        if (offsetRequired) {
            paddingLeft += getLeftPaddingOffset();
        }

        int left = mScrollX + paddingLeft;
        int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
        int top = mScrollY + getFadeTop(offsetRequired);
        int bottom = top + getFadeHeight(offsetRequired);

        if (offsetRequired) {
            right += getRightPaddingOffset();
            bottom += getBottomPaddingOffset();
        }

        final ScrollabilityCache scrollabilityCache = mScrollCache;
        final float fadeHeight = scrollabilityCache.fadingEdgeLength;
        int length = (int) fadeHeight;

        // clip the fade length if top and bottom fades overlap
        // overlapping fades produce odd-looking artifacts
        if (verticalEdges && (top + length > bottom - length)) {
            length = (bottom - top) / 2;
        }

        // also clip horizontal fades if necessary
        if (horizontalEdges && (left + length > right - length)) {
            length = (right - left) / 2;
        }

        if (verticalEdges) {
            topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
            drawTop = topFadeStrength * fadeHeight > 1.0f;
            bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
            drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
        }

        if (horizontalEdges) {
            leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
            drawLeft = leftFadeStrength * fadeHeight > 1.0f;
            rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
            drawRight = rightFadeStrength * fadeHeight > 1.0f;
        }

        saveCount = canvas.getSaveCount();

        int solidColor = getSolidColor();
        if (solidColor == 0) {
            if (drawTop) {
                canvas.saveUnclippedLayer(left, top, right, top + length);
            }

            if (drawBottom) {
                canvas.saveUnclippedLayer(left, bottom - length, right, bottom);
            }

            if (drawLeft) {
                canvas.saveUnclippedLayer(left, top, left + length, bottom);
            }

            if (drawRight) {
                canvas.saveUnclippedLayer(right - length, top, right, bottom);
            }
        } else {
            scrollabilityCache.setFadeColor(solidColor);
        }

        // 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
        final Paint p = scrollabilityCache.paint;
        final Matrix matrix = scrollabilityCache.matrix;
        final Shader fade = scrollabilityCache.shader;

        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);
        }

        if (drawBottom) {
            matrix.setScale(1, fadeHeight * bottomFadeStrength);
            matrix.postRotate(180);
            matrix.postTranslate(left, bottom);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, bottom - length, right, bottom, p);
        }

        if (drawLeft) {
            matrix.setScale(1, fadeHeight * leftFadeStrength);
            matrix.postRotate(-90);
            matrix.postTranslate(left, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(left, top, left + length, bottom, p);
        }

        if (drawRight) {
            matrix.setScale(1, fadeHeight * rightFadeStrength);
            matrix.postRotate(90);
            matrix.postTranslate(right, top);
            fade.setLocalMatrix(matrix);
            p.setShader(fade);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }
    }
複製代碼

從上面的源碼分析得知,View的draw()方法總共分爲6步:
1,繪製視圖的背景;
2,若是須要,保存畫布的圖層以備漸變用;
3,繪製當前視圖的內容;
4,繪製子View的視圖;
5,若是須要,繪製視圖的漸變效果並恢復畫布;
6,繪製裝飾(好比滾動條scrollbars);

總結爲流程圖以下:

這裏須要關注的是第二步和第四步,第二步是經過調用onDraw()方法繪製當前視圖的內容,第四步是調用dispatchDraw()來繪製子View的視圖;

先來看一下View的onDraw()方法:

protected void onDraw(Canvas canvas) {}
複製代碼

是一個空方法,交由子類去實現;若是實現來自定義View,那麼就得從新該方法去實現繪製的邏輯;

再看一下dispatchDraw()方法:

protected void dispatchDraw(Canvas canvas) {}
複製代碼

這個方法也是個空方法,也是要交由子類去實現;

查看源碼的實現有不少個,這裏咱們只關注ViewGroup的實現邏輯;

ViewGroup的繪製

看一下ViewGroup的實現邏輯:

protected void dispatchDraw(Canvas canvas) {
        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                // 遍歷子View設置動畫效果
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    bindLayoutAnimation(child);
                }
            }
            ...
        }

        ...
        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) {
                    // 遍歷子View進行繪製
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
               ...
            }

            ...
        }
        ...
    }
複製代碼

源碼裏面經過遍歷全部的子View,調用drawChild()來進行繪製,繼續跟進drawChild()方法裏面;

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
}
複製代碼

這裏又看到了熟悉的View,調用了View的draw()方法繪製子View;

到這裏,performDraw()方法的流程就講完了,看一下總結流程圖:

總結:
1,View樹的繪製流程也是經過遞歸的方式來進行繪製的,遞歸的方法爲View的draw()方法;
2,ViewGroup和View均可以從新onDraw()方法來實現繪製的邏輯,子View不須要重寫dispatchDraw()方法;
3,繪製視圖的背景,漸變效果和裝飾都是在View的draw()方法裏面調用的;

最後總結:

performTraversals()方法的流程分析完畢了,如今終於知道了View的繪製流程爲何分爲onMeasure(),onLayout(),onDraw()這三個步驟了;貼出來的源碼省略了不少細節,主要是爲了把繪製的流程理清,建議能夠本身跟着源碼去走一遍;

參考:

1,www.jianshu.com/p/58d22426e…
2,blog.csdn.net/feiduclear_…
3,blog.csdn.net/luoshengyan…
4,www.jianshu.com/p/4a68f9dc8…
5,www.2cto.com/kf/201512/4…

關於我

若是個人文章對你有幫助的話,請給我點個❤️,也能夠關注一下個人Github博客;

歡迎和我溝通交流技術;

相關文章
相關標籤/搜索