Android View的測(cè)量、布局、繪制流程源碼分析及自定義View實(shí)例演示

當(dāng)Android原生控件無法滿足需求時(shí)就要自定義View,只有掌握了View的測(cè)量過程 (measure)、布局過程(layout)和繪制過程(draw)過程才能自定義出復(fù)雜的View。

預(yù)備知識(shí)

頂層視圖(DecorView)及其所關(guān)聯(lián)的ViewRoot對(duì)象的創(chuàng)建過程,如下圖所示(參考文檔1):



上圖中第9步獲取到的就是頂層視圖decor,第11、12、13步就是將decor傳遞給ViewRoot,這樣ViewRoot就和DecorView建立了關(guān)聯(lián)。
在第13步中,ViewRoot類的成員函數(shù)setView會(huì)調(diào)用ViewRoot類的另外一個(gè)成員函數(shù)requestLayout,該函數(shù)會(huì)對(duì)頂層視圖(DecorView)觸發(fā)第一次測(cè)量過程 (measure)、布局過程(layout)和繪制過程(draw)。接下來就從requestLayout開始分析:



上圖中的第5步會(huì)調(diào)用ViewRootImpl類的performTraversals方法,performTraversals方法會(huì)依次調(diào)用performMeasure方法、performLayout方法和performDram方法來完成頂層視圖decor的測(cè)量過程 (measure)、布局過程(layout)和繪制過程(draw)。

View的測(cè)量過程 (measure)

上圖的第9步會(huì)遍歷每一個(gè)子View,并且調(diào)用子View的measure方法對(duì)子View進(jìn)行測(cè)量(即第10步)。非ViewGroup類型的View通過onMeasure方法就完成了其測(cè)量過程,而ViewGroup類型的View除了通過onMeasure方法就完成自身的測(cè)量過程外,還要在onMeasure方法中完成遍歷子View并且調(diào)用子View的measure方法對(duì)子View進(jìn)行測(cè)量。

  1. 非ViewGroup類型的View的測(cè)量過程


    View的測(cè)量時(shí)序圖.jpg

    對(duì)于上面的步驟進(jìn)行解析一下,第1步執(zhí)行View類中的measure方法,該方法是一個(gè)final方法,這就意味著子類不能從寫該方法,measure方法會(huì)調(diào)用View類的onMeasure方法,onMeasure方法的實(shí)現(xiàn)代碼如下所示:

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

從上面的代碼就對(duì)應(yīng)上圖中3、4、5、6、7步,先來看第3步對(duì)應(yīng)的View類的getSuggestedMinimumWidth方法的源碼:

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

從getSuggestedMinimumWidth的代碼可以看出,當(dāng)View沒有設(shè)置背景,那么getSuggestedMinimumWidth方法的返回值為mMinWidth,而mMinWidth對(duì)應(yīng)于android: minWidth屬性指定的值,如果沒有設(shè)置android: minWidth屬性,則mMinWidth默認(rèn)為0;如果View設(shè)置了背景,則getSuggestedMinimumWidth方法的返回值為max(mMinWidth, mBackground.getMinimumWidth()),下面先來看看Drawable類中g(shù)etMinimumWidth方法的源碼:

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

有上面的代碼可知getMinimumWidth返回的是View的背景的原始寬度,如果View的背景沒有原始寬度,就返回0。

現(xiàn)在來總結(jié)一下getSuggestedMinimumWidth方法的邏輯,當(dāng)View沒有設(shè)置背景時(shí),getSuggestedMinimumWidth方法的返回值為android: minWidth屬性指定的值,這個(gè)值可以為0;當(dāng)View設(shè)置了背景時(shí),getSuggestedMinimumWidth方法的返回值為android: minWidth屬性指定的值與View的背景的最小寬度中的最大值。
現(xiàn)在我們來看一下最關(guān)鍵的View類的getDefaultSize方法的源代碼(對(duì)應(yīng)第4步):

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

上面的邏輯很簡(jiǎn)單,對(duì)于MeasureSpec.AT_MOST和MeasureSpec.EXACTLY測(cè)量模式,getDefaultSize直接返回測(cè)量后的值(所以直接繼承View的自定義控件需要重寫onMeasure方法并且設(shè)置wrap_content時(shí)的自身大小,否者在布局中使用wrap_content就相當(dāng)于使用math_parent);對(duì)于MeasureSpec.UNSPECIFIED測(cè)量模式,一般用于系統(tǒng)內(nèi)部的測(cè)量過程,getDefaultSize返回值為getSuggestedMinimumWidth方法的返回值。對(duì)于第5、6步與3、4步類似,這里就不再綴續(xù)了。

第7步中View類的setMeasuredDimension方法調(diào)用了第8步中View類的setMeasuredDimensionRaw方法,setMeasuredDimensionRaw方法的源碼:

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

有上面的代碼可知,View測(cè)量后的寬高被保存到View類的成員變量mMeasuredWidth和mMeasuredHeight中了,通過View類的getMeasuredWidth方法和getMeasuredHeight方法獲取的就是mMeasuredWidth和mMeasuredHeight的值,需要注意的是,在某些極端情況下,系統(tǒng)可能需要多次measure才能確定最終的測(cè)量寬高,在這種情況下,在onMeasure方法中拿到的測(cè)量寬高很可能是不準(zhǔn)確的,一個(gè)好的習(xí)慣是在onLayout方法中去獲取View最終的測(cè)量寬高。

  1. ViewGroup類型的View的測(cè)量過程


    ViewGroup的測(cè)量時(shí)序圖.jpg

    ViewGroup并沒有定義其自身測(cè)量的具體過程(即沒有onMeasure方法),這是因?yàn)閂iewGroup是一個(gè)抽象類,其測(cè)量過程的onMeasure方法需要各個(gè)子類去具體實(shí)現(xiàn),所以上面展示了LinearLayout測(cè)量流程圖。

上圖第1步執(zhí)行View類中的measure方法,該方法是一個(gè)final方法,這就意味著子類不能從寫該方法,measure方法會(huì)調(diào)用LinearLayout類的onMeasure方法,onMeasure方法的實(shí)現(xiàn)代碼如下所示:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

當(dāng)前分析當(dāng)LinearLayout的方向是垂直方向的情況,此時(shí)會(huì)執(zhí)行LinearLayout類的measureVertical方法:

// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
    final View child = getVirtualChildAt(i);
    // Determine how big this child would like to be. If this or
    // previous children have given a weight, then we allow it to
    // use all available space (and we will shrink things later
    // if needed).
......
    measureChildBeforeLayout(
           child, i, widthMeasureSpec, 0, heightMeasureSpec,
           totalWeight == 0 ? mTotalLength : 0);
    
    if (oldHeight != Integer.MIN_VALUE) {
       lp.height = oldHeight;
    }
    
    final int childHeight = child.getMeasuredHeight();
    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
           lp.bottomMargin + getNextLocationOffset(child));
......
}
......
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
......
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        heightSizeAndState);
.....

首先measureVertical方法會(huì)遍歷每一個(gè)子元素并且執(zhí)行measureChildBeforeLayout方法對(duì)子元素進(jìn)行測(cè)量,measureChildBeforeLayout方法內(nèi)部會(huì)執(zhí)行子元素的measure方法。在代碼中,變量mTotalLength會(huì)是用來存放LinearLayout在豎直方向上的當(dāng)前高度,每遍歷一個(gè)子元素,mTotalLength就會(huì)增加,增加的部分主要包括子元素自身的高度、子元素在豎直方向上的margin。

當(dāng)測(cè)量完所有子元素時(shí),就會(huì)很容易得到LinearLayout自身的大小,對(duì)于豎直的LinearLayout,水平方向的寬度等于最寬元素的寬度加上左右的padding,如果高度采用的是math_content或者具體數(shù)值,那么它的高度為父布局的給到的高度或者具體數(shù)值,如果高度采用的是wrap_content,那么高度是所有子元素所占用的高度總和加上上下padding 并且 能超過父容器的剩余空間,這個(gè)過程對(duì)應(yīng)與resolveSizeAndState的源碼:

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

下面我們來看一看LinearLayout類的measureChildBeforeLayout方法是如何對(duì)子元素進(jìn)行測(cè)量,該方法的第第4個(gè)和第6個(gè)參數(shù)分別代表在水平方向和垂直方向上LinearLayout已經(jīng)被其他子元素占據(jù)的長(zhǎng)度,measureChildBeforeLayout的源碼如下:

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

LinearLayout類的measureChildBeforeLayout方法會(huì)調(diào)用ViewGroup類的
measureChildWithMargins方法,measureChildWithMargins方法的源碼如下:

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

ViewGroup類的measureChildWithMargins方法會(huì)調(diào)用子元素的measure方法對(duì)子元素進(jìn)行測(cè)量,在對(duì)子元素測(cè)量之前先會(huì)通過調(diào)用ViewGroup類的getChildMeasureSpec方法得到傳遞給子元素的MeasureSpec(即能給到子元素的空間),從getChildMeasureSpec方法的前二個(gè)參數(shù)可知,子元素MeasureSpec的創(chuàng)建與父容器的MeasureSpec、父容器的padding、子元素的margin和兄弟元素占用的長(zhǎng)度有關(guān)。

ViewGroup類的getChildMeasureSpec方法代碼如下所示:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can't be
            // bigger than us.
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

ViewGroup類的getChildMeasureSpec方法的邏輯可以通過下表來說明,注意,表中的parentSize是指父容器目前可使用的大?。▍⒖糀ndroid開發(fā)藝術(shù)探索182頁):

childLayoutParams/parentSpecMode EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY/childSize EXACTLY/childSize EXACTLY/childSize
MATCH_PARENT EXACTLY/parentSize AT_MOST/parentSize UNSPECIFIED/0
WRAP_CONTENT AT_MOST/parentSize AT_MOST/parentSize UNSPECIFIED/0

ViewGroup類的getChildMeasureSpec方法返回子元素寬高的MeasureSpec,然后將子元素寬高的MeasureSpec作為measure方法的參數(shù)。

到此為止,非ViewGroup類型的View的測(cè)量過程和ViewGroup類型的View的測(cè)量過程已經(jīng)分析完畢,進(jìn)行如下總結(jié):
1> 父View會(huì)遍歷測(cè)量每一個(gè)子View(通常使用ViewGroup類的measureChildWithMargins方法),該方法會(huì)調(diào)用子View的measure方法并且將父布局剩余空間構(gòu)建的寬高(通過getChildMeasureSpec方法)作為measure方法的參數(shù)。
2> 非ViewGroup類型的View自身的測(cè)量是在非ViewGroup類型的View的onMeasure方法中進(jìn)行測(cè)量的
3> ViewGroup類型的View自身的測(cè)量是在ViewGroup類型View的onMeasure方法中進(jìn)行測(cè)量的
4>直接繼承ViewGroup的自定義控件需要重寫onMeasure方法并且設(shè)置wrap_content時(shí)的自身大小,否者在布局中使用wrap_content就相當(dāng)于使用math_parent,具體原因通過上面的表格可以說明。

View的布局過程(layout)

decor的三大流程圖的第16步會(huì)遍歷并且調(diào)用子元素的layout方法,layout過程比measure過程簡(jiǎn)單多了,layout方法用來確定View本身的位置,而onLayout方法用來確定所有子元素的位置。

ViewGroup類型的View和非ViewGroup類型的View的布局過程是不同的,非ViewGroup類型的View通過layout方法就完成了其布局過程,而ViewGroup類型的View除了通過layout方法就完成自身的布局過程外,還要調(diào)用onLayout方法去遍歷子元素并且調(diào)用子元素的layout方法,各個(gè)子View再去遞歸執(zhí)行這個(gè)流程。

  1. 非ViewGroup類型的View的布局過程


    View的布局時(shí)序圖.jpg

    對(duì)上面的時(shí)序圖進(jìn)行一下解析,第1步執(zhí)行View類的layout方法,代碼如下:

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

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

    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

由于setOpticalFrame()內(nèi)部會(huì)調(diào)用setFrame(),所以最終都是通過setFrame()方法設(shè)置布局的位置的。

接下來看下setFrame方法:

protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;

    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;

        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

        // Invalidate our old position
        invalidate(sizeChanged);

        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;

        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
    }
    return changed;
}

由上面的源碼可知,setFrame方法是用來設(shè)定View的四個(gè)頂點(diǎn)的位置,即設(shè)置mLeft、mTop、mRight、mBottom這四個(gè)值,View的四個(gè)頂點(diǎn)一旦確定,那么View在父容器中的位置也就確定了。

第3步layout方法接著調(diào)用View類的onLayout方法,這個(gè)方法的作用是用來確定子元素的位置,由于非ViewGroup類型的View沒有子元素,所以View類的onLayout方法為空。

  1. ViewGroup類型的View的布局過程
    ViewGroup的布局時(shí)序圖.jpg

    上面其實(shí)是LinearLayout的布局時(shí)序圖,因?yàn)閂iewGroup的onLayout方法是抽象方法,所以就選擇了ViewGroup的子類LinearLayout進(jìn)行分析。

上圖第1步執(zhí)行ViewGroup類的layout方法,該方法是一個(gè)final方法,即子類無法重寫該方法,源代碼如下:

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        super.layout(l, t, r, b);
    } else {
        // record the fact that we noop'd it; request layout when transition finishes
        mLayoutCalledWhileSuppressed = true;
    }
}

第2步ViewGroup類的layout方法會(huì)調(diào)用View類的layout方法,第3步View類的layout方法調(diào)用View類的setFrame方法,這兩步與上面討論非ViewGroup類型的View的布局過程的第1、2步相同,這里就不在贅敘,直接看第4步View類的layout方法調(diào)用LinearLayout類的onLayout方法:

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

當(dāng)前分析LinearLayout的方向是垂直方向的場(chǎng)景,layoutVertical方法如下:

void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;
    
    // Where right end of child should go
    final int width = right - left;
    int childRight = width - mPaddingRight;
    
    // Space available for child
    int childSpace = width - paddingLeft - mPaddingRight;
    
    final int count = getVirtualChildCount();

    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

    switch (majorGravity) {
       case Gravity.BOTTOM:
           // mTotalLength contains the padding already
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;

           // mTotalLength contains the padding already
       case Gravity.CENTER_VERTICAL:
           childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
           break;

       case Gravity.TOP:
       default:
           childTop = mPaddingTop;
           break;
    }

    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();
            
            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

可以看到onLayout方法會(huì)遍歷每一個(gè)子元素并且調(diào)用setChildFrame方法,setChildFrame方法會(huì)調(diào)用子元素的layout方法來對(duì)子元素進(jìn)行布局,setChildFrame方法的源碼如下:

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

View的繪制過程(draw)

decor的三大流程圖的第23步會(huì)遍歷每一個(gè)子View并且調(diào)用子元素的draw方法,繼而開始進(jìn)行子View的繪制過程。先通過如下的時(shí)序圖,整體的看一下繪制過程:


ViewGroup的繪制時(shí)序圖.jpg

上面其實(shí)是LinearLayout的繪制時(shí)序圖,因?yàn)閂iew的onDraw方法是空方法,所以就選擇了ViewGroup的子類LinearLayout進(jìn)行分析。

LinearLayout的繪制過程遵循如下幾步:
1> 繪制背景
2> 繪制自己(繪制分割線)
3> 繪制子View(dispatchDraw)
4> 繪制前景
Android中是通過View類的draw方法來實(shí)現(xiàn)上面的4步,源碼如下所示:

/**
 * Manually render this view (and all of its children) to the given Canvas.
 * The view must have already done a full layout before this function is
 * called.  When implementing a view, implement
 * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method.
 * If you do need to override this method, call the superclass version.
 *
 * @param canvas The Canvas to which the View is rendered.
 */
@CallSuper
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

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

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

        // we're done...
        return;
    }
.....
}

從這個(gè)方法的注釋可以知道,當(dāng)自定義View并且需要繪制時(shí),應(yīng)該重寫View類的onDraw方法而不要重寫View類的draw方法,如果你需要重寫draw方法,必須在重寫時(shí)調(diào)用父類的draw方法。上面的代碼很明顯的驗(yàn)證了View繪制過程的4步。由于View類無法確定自己是否有子元素,所以View類的dispatchDraw方法是空方法,那么我們就來看看ViewGroup類的dispatchDraw方法的源碼(由于該方法的源碼太長(zhǎng)了,因此我只展示我們感興趣的部分代碼):

@Override
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    ......
    boolean more = false;
    final long drawingTime = getDrawingTime();

    if (usingRenderNodeProperties) canvas.insertReorderBarrier();
    final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
    int transientIndex = transientCount != 0 ? 0 : -1;
    // Only use the preordered list if not HW accelerated, since the HW pipeline will do the
    // draw reordering internally
    final ArrayList<View> preorderedList = usingRenderNodeProperties
            ? null : buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    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);
        }
    }
......
}

ViewGroup類的dispatchDraw方法會(huì)遍歷每一個(gè)子元素,然后調(diào)用ViewGroup類的drawChild方法對(duì)子元素進(jìn)行繪制,ViewGroup類的drawChild方法源碼如下:

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

自定義View實(shí)例

  1. 自定義View的分類
    1> 通過繼承View或者ViewGroup實(shí)現(xiàn)自定義View
    2> 通過繼承已有的控件實(shí)現(xiàn)自定義View
    3> 通過組合實(shí)現(xiàn)自定義View
    我在下面只針對(duì)1>來實(shí)現(xiàn)自定義View,因?yàn)?>和3>相對(duì)于1>就比較簡(jiǎn)單了。
  2. 通過繼承View實(shí)現(xiàn)環(huán)狀進(jìn)度條
    首先展示一下效果圖:


    環(huán)狀進(jìn)度條

    下面就來分析一下實(shí)現(xiàn)代碼:
    根據(jù)上面對(duì)非ViewGrop類型View三大流程的分析,第一步就是測(cè)量,
    由于是繼承View類的,因此如果想要支持wrap_content屬性,就必須重寫onMeasure方法,如下所示(可以當(dāng)做模板代碼):

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mHeight);
    } else {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

第二步就是進(jìn)行布局,由于非ViewGrop類型View自身的布局在View類的layout方法中已經(jīng)實(shí)現(xiàn),而onLayout方法是用來對(duì)子View進(jìn)行布局的,所以對(duì)于非ViewGrop類型View就不用考慮布局的實(shí)現(xiàn)。
第三步就是進(jìn)行繪制,由于非ViewGrop類型View沒有子View,所以不用考慮對(duì)子View的繪制,因此只要重寫View類的onDraw方法對(duì)自身進(jìn)行繪制即可,代碼如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawArc(new RectF(getPaddingLeft(), getPaddingTop(), mWidth - getPaddingRight(), mHeight - getPaddingBottom()), 0, sweepValue, false, paint);
}

從上面的代碼中可以看出,如果不在onDraw方法中處理padding,那么padding屬性無法起作用。

  1. 通過繼承ViewGroup實(shí)現(xiàn)流式布局(FlowLayout)
    首先展示一下效果圖:


    流式布局

    下面就來分析一下實(shí)現(xiàn)代碼:
    根據(jù)上面對(duì)ViewGrop類型View三大流程的分析,第一步就是測(cè)量,
    由于是繼承ViewGrop類的,因此如果想要支持wrap_content屬性,就必須重寫onMeasure方法,代碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    mLineWidths.clear();
    mLineHeights.clear();
    mLineViewNums.clear();
    int width = 0;
    int lineWidth = 0;
    int height = 0;
    int lineHeight = 0;
    int lineViewNum = 0;

    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View childView = getChildAt(i);

        if (View.GONE == childView.getVisibility()) {
            if (i == childCount - 1) {
                lineViewNum++;
                mLineViewNums.add(lineViewNum);
                mLineWidths.add(lineWidth);
                width = Math.max(width, lineWidth);
                mLineHeights.add(lineHeight);
                height += lineHeight;
            }
            continue;
        }

        MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
        measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
        if (lineWidth + childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin > widthSpecSize - getPaddingLeft() - getPaddingRight()) {
            mLineViewNums.add(lineViewNum);
            lineViewNum = 1;
            mLineWidths.add(lineWidth);
            lineWidth = childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            mLineHeights.add(lineHeight);
            height += lineHeight;
            lineHeight = Math.max(lineHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        } else {
            lineViewNum++;
            lineWidth += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            width = Math.max(width, lineWidth);
            lineHeight = Math.max(lineHeight, childView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
        }
    }
    mLineViewNums.add(lineViewNum);
    mLineWidths.add(lineWidth);
    mLineHeights.add(lineHeight);
    height += lineHeight;

    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), height + getPaddingTop() + getPaddingBottom());
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(width + getPaddingLeft() + getPaddingRight(), heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, height + getPaddingTop() + getPaddingBottom());
    } else {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

上面代碼的邏輯:遍歷每一個(gè)子元素,然后通過measureChildWithMargins方法對(duì)子元素進(jìn)行測(cè)量,注意第3個(gè)和第5個(gè)參數(shù)必須是0,因?yàn)槲沂窍朐诟冈厮加械目臻g中為子元素進(jìn)行測(cè)量,在遍歷每個(gè)子元素的過程中,記錄每一行的最終寬度、最終高度和每一行的子元素個(gè)數(shù)。
第二步就是進(jìn)行布局,由于ViewGrop類型View自身的布局在ViewGrop類的layout方法中已經(jīng)實(shí)現(xiàn),ViewGrop類的layout方法會(huì)調(diào)用ViewGrop類的onLayout方法,由于ViewGrop類的onLayout方法是抽象的,所以必須實(shí)現(xiàn)onLayout方法并且實(shí)現(xiàn)對(duì)子View的布局,代碼如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int lineNum = mLineWidths.size();
    int paddingTop = getPaddingTop();
    int startIndex = 0;
    int endIndex = 0;
    for (int line = 0; line < lineNum; line++) {
        int paddingLeft = 0;
        int currentLineWidth = mLineWidths.get(line);
        switch (mGravity) {
            case LEFT:
                paddingLeft = getPaddingLeft();
                break;
            case CENTER:
                paddingLeft = (getWidth() - currentLineWidth)/2;
                break;
            case RIGHT:
                paddingLeft = getWidth() - currentLineWidth - getPaddingRight();
                break;
        }

        endIndex += mLineViewNums.get(line);
        for (; startIndex < endIndex; startIndex++) {
            View childView = getChildAt(startIndex);

            if (View.GONE == childView.getVisibility()) {
                continue;
            }

            MarginLayoutParams lp = (MarginLayoutParams) childView.getLayoutParams();
            int lc = paddingLeft + lp.leftMargin;
            int tc = paddingTop + lp.topMargin;
            int rc = childView.getMeasuredWidth() + lc;
            int bc = childView.getMeasuredHeight() + tc;
            childView.layout(lc, tc, rc, bc);
            paddingLeft += childView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
        }
        paddingTop += mLineHeights.get(line);
    }
}

上面代碼的邏輯:逐行遍歷每一個(gè)子View并且計(jì)算出子View的左上角和右下角的坐標(biāo),然后調(diào)用子View的layout方法對(duì)子View進(jìn)行布局。
第三步就是進(jìn)行繪制,由于我現(xiàn)在設(shè)計(jì)的流式布局不需要對(duì)自己進(jìn)行繪制,所以不用考慮繪制。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容