1. 程式人生 > >LinearLayout線性佈局measure流程原始碼分析

LinearLayout線性佈局measure流程原始碼分析

1. 介紹
LinearLayout線性佈局在Android開發中使用頻率很高,它包含的子控制元件將會按照橫向或者豎向的方向順序排列。線性佈局的使用在不涉及weight的情況下比較簡單,此篇主要通過分析LinearLayout原始碼理清線性佈局的measure流程以及weight所發揮的作用!

2.原始碼分析
LinearLayout線性佈局的measure操作按照不同的orientation呼叫不同的方法進行,但是原理是一樣的。

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

我們這裡只分析豎直方向的measure操作,在這之前我們先看一個簡單的事例

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" >
<TextView android:layout_width="match_parent" android:layout_height="20dp" android:layout_weight="1"
android:background="#FF0000" android:gravity="center" android:text="第一個" />
<TextView android:layout_width="match_parent" android:layout_height="match_parent" android:background="#00FF00" android:gravity="center" android:text="第二個" /> </LinearLayout>

像這種佈局結構,第二個TextView宣告自己填充整個高度,雖然不知道最後怎麼顯示,但是第一個TextView因該至少為顯示20dp的高度,但是結果是第一個TextView不會顯示
這裡寫圖片描述
為什麼會這樣呢,我們來看下measureVertical的原始碼

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
        mTotalLength = 0;
        int maxWidth = 0;
        int alternativeMaxWidth = 0;
        int weightedMaxWidth = 0;
        boolean allFillParent = true;
        float totalWeight = 0;

        final int count = getVirtualChildCount();

        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchWidth = false;

        final int baselineChildIndex = mBaselineAlignedChildIndex;        
        final boolean useLargestChild = mUseLargestChild;

        int largestChildHeight = Integer.MIN_VALUE;

        // See how tall everyone is. Also remember max width.
        for (int i = 0; i < count; ++i) {
            final View child = getVirtualChildAt(i);

            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }

            if (child.getVisibility() == View.GONE) {
               i += getChildrenSkipCount(child, i);
               continue;
            }

            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

            totalWeight += lp.weight;

            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                // Optimization: don't bother measuring children who are going to use
                // leftover space. These views will get measured again down below if
                // there is any leftover space.
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            } else {
                int oldHeight = Integer.MIN_VALUE;

                if (lp.height == 0 && lp.weight > 0) {
                    // heightMode is either UNSPECIFIED or AT_MOST, and this
                    // child wanted to stretch to fill available space.
                    // Translate that to WRAP_CONTENT so that it does not end up
                    // with a height of 0
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }

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

                if (useLargestChild) {
                    largestChildHeight = Math.max(childHeight, largestChildHeight);
                }
            }

            /**
             * If applicable, compute the additional offset to the child's baseline
             * we'll need later when asked {@link #getBaseline}.
             */
            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
               mBaselineChildTop = mTotalLength;
            }

            // if we are trying to use a child index for our baseline, the above
            // book keeping only works if there are no children above it with
            // weight.  fail fast to aid the developer.
            if (i < baselineChildIndex && lp.weight > 0) {
                throw new RuntimeException("A child of LinearLayout with index "
                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
                        + "won't work.  Either remove the weight, or don't set "
                        + "mBaselineAlignedChildIndex.");
            }

            boolean matchWidthLocally = false;
            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
                // The width of the linear layout will scale, and at least one
                // child said it wanted to match our width. Set a flag
                // indicating that we need to remeasure at least that view when
                // we know our width.
                matchWidth = true;
                matchWidthLocally = true;
            }

            final int margin = lp.leftMargin + lp.rightMargin;
            final int measuredWidth = child.getMeasuredWidth() + margin;
            maxWidth = Math.max(maxWidth, measuredWidth);

            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /*
                 * Widths of weighted Views are bogus if we end up
                 * remeasuring, so keep them separate.
                 */
                weightedMaxWidth = Math.max(weightedMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            } else {
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);
            }

            i += getChildrenSkipCount(child, i);
        }

        if (useLargestChild && heightMode == MeasureSpec.AT_MOST) {
            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }

                if (child.getVisibility() == GONE) {
                    i += getChildrenSkipCount(child, i);
                    continue;
                }

                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                        child.getLayoutParams();
                // Account for negative margins
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
                        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
        heightSize = resolveSize(heightSize, heightMeasureSpec);

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds
        int delta = heightSize - mTotalLength;
        if (delta != 0 && totalWeight > 0.0f) {
            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            mTotalLength = 0;

            for (int i = 0; i < count; ++i) {
                final View child = getVirtualChildAt(i);

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

                LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();

                float childExtra = lp.weight;
                if (childExtra > 0) {
                    // Child said it could absorb extra space -- give him his share
                    int share = (int) (childExtra * delta / weightSum);
                    weightSum -= childExtra;
                    delta -= share;

                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            mPaddingLeft + mPaddingRight +
                                    lp.leftMargin + lp.rightMargin, lp.width);

                    // TODO: Use a field like lp.isMeasured to figure out if this
                    // child has been previously measured
                    if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
                        // child was measured once already above...
                        // base new measurement on stored values
                        int childHeight = child.getMeasuredHeight() + share;
                        if (childHeight < 0) {
                            childHeight = 0;
                        }

                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
                    } else {
                        // child was skipped in the loop above.
                        // Measure for this first time here      
                        child.measure(childWidthMeasureSpec,
                                MeasureSpec.makeMeasureSpec(share > 0 ? share : 0,
                                        MeasureSpec.EXACTLY));
                    }
                }

                final int margin =  lp.leftMargin + lp.rightMargin;
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);

                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
                        lp.width == LayoutParams.MATCH_PARENT;

                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                        matchWidthLocally ? margin : measuredWidth);

                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;

                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
            }

            // Add in our padding
            mTotalLength += mPaddingTop + mPaddingBottom;
            // TODO: Should we recompute the heightSpec based on the new total length?
        } else {
            alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                           weightedMaxWidth);
        }

        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
            maxWidth = alternativeMaxWidth;
        }

        maxWidth += mPaddingLeft + mPaddingRight;

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

        setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);

        if (matchWidth) {
            forceUniformWidth(count, heightMeasureSpec);
        }
    }

在這個方法裡面有幾個比較重要的變數
@mTotalLength 代表測量過的子佈局的高度和
@totalWeight 代表所有字佈局weight和
@heightMode線性佈局父佈局傳遞給線性佈局的測量Mode
@mWeightSum 代表線性佈局weightsum屬性,預設為-1

其中2-20行:初始化變數
其中22 - 150行:在這個迴圈裡面遍歷子View,有選擇性對某個view進行measure或者不進行measure操作
@其中39-44行:針對heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0的情況,符合這個條件的子view在這裡不被測量,當最後高度還有結餘的時候,才有可能分享結餘高度被顯示
@其中46-72行: 這裡會把符合lp.height == 0 && lp.weight > 0條件的子View的lp.height = LayoutParams.WRAP_CONTENT;因為下面對weight進行處理的時候,會判斷lp.height來區分該View是否已經測量過,在這裡你可能會有一個疑問,為什麼符合heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0不需要測量,而除了符合heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0的view都需要測量呢,這個在下面會解釋
然後對子View進行測量,只要這個子View或者之前的view的weight屬性不是0就會按照0為引數作為使用使用的高度測量該view,具體細節請檢視measureChildWithMargins方法,最後將測量後的高度累加到mTotalLength 中。

其中155-161行:這裡通過resolveSize方法確定heightSize

public static int resolveSize(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:
            result = Math.min(size, specSize);
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

通過這個方法可以看出,假如heightMode為EXACTLY的時候,最後的result是一個確定的值,而那些
heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0的子View明確是想分享剩餘的高度,因為最後剩餘多少未知,所以在上面那個疑問中是沒有必要對這些View進行測量的,最後真的有剩餘高度的情況下才進行測量,但是當heightMode為AT_MOST的時候,result為Math.min(size, specSize)所有都要測量。

其中165-210行:比較heightSize和mTotalLength的大小差值,對於符合(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)的子View如果還有高度結餘則會按照weight比例分享高度結餘,如果高度還不夠,那麼這些view需要按照weight比例奉獻出指定的高度,所以對於weight屬性也算是一把雙刃劍

最後246行:通過setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize)以heightsize為引數,確定LinearLayout的高度和寬度

3. 結束語
通過上面的原始碼分析,可以解釋上面的例項,在第一次迴圈,TextView獲得20dp高度,TextView2獲得父佈局最大高度,這樣mTotalLength = 父佈局最大高度 + 20dp
heightSize = 父佈局最大高度,所以TextView要貢獻出int delta = heightSize - mTotalLength= -20dp
所以TextView沒有被顯示。