Android 8.0 LinearLayout 源码解析

作者: lijiankun24 | 来源:发表于2019-03-18 09:04 被阅读684次

    一. 开篇

    在最开始接触 Android 开发的时候便学习了 LinearLayout 布局控件,它可以在垂直/水平方向依次展开 childView,再配合 weight 属性使用的话,可以高效、方便地完成许多 UI 界面的开发。其实 LinearLayout 还有一些其他用法,可能用的不多,可以参考这篇文章 你对LinearLayout到底有多少了解?(一)-属性篇
    以前就知道,在 LinearLayout 布局时,如果不使用 weight 属性,LinearLayout 中每个 childView 只会测量一次,如果使用 weight 属性,每个 childView 会测量两次,分析了源码之后,发现这种说法也不是十分准确,childView 会不会被测量两次,除了依赖是否设置 android:layout_weight 属性,还需要依赖其他属性的

    二. 源码解析

    在 LinearLayout 中有垂直/水平两个方向的布局,任一方向的布局思想都是相同的,所以我们只需要具体分析其中一个方向即可,另一个方向可以类比,在这里我们分析垂直方向的思想

    在 View 和 ViewGroup 中的布局有三大流程,分别是 onMeasureonLayoutonDraw,在 LinearLayout 中 onLayoutonDraw 两个流程基本都是模板化的写法,而且 LinearLayout 布局简单,无论是垂直方向还是水平方向都是依次排列每个 childView 的,分析起来并不复杂,大家可以自行分析。
    但是 onMeasure 流程就比较复杂,分为两种情况:

    • 不使用 layout_weight 属性,每个 childView 按照自身的情况计算本身的大小即可
    • 使用 layout_weight 属性,需要根据 LinearLayout 的剩余空间和 layout_weight 的比例,计算每个 childView 的大小

    Ok, let's fuck the source code

    2.1 非 weight 的情况

    2.1.1 布局文件 & 效果

    首先,我们来看一个简单的布局,xml 文件如下所示

    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">
    
        <TextView
                android:layout_width="match_parent"
                android:layout_height="200dp"
                android:text="TextView1"
                android:gravity="center"
                android:textSize="24sp"
                android:textColor="@android:color/white"
                android:background="@android:color/holo_green_light"/>
    
        <TextView
                android:layout_width="match_parent"
                android:layout_height="300dp"
                android:text="TextView2"
                android:gravity="center"
                android:textSize="24sp"
                android:textColor="@android:color/white"
                android:background="@android:color/holo_blue_light"/>
    </LinearLayout>
    

    其中,两个 TextView 都没有设置 layout_weight 属性,第一个 TextView 的 layout_height 属性是 200dp,第二个 TextView 的 layout_height300dp,我想这样简单的布局只要稍微懂 Android 开发的人都知道是什么样的,它的效果如下图所示,但是说到它的源码执行,不知道又有多少人可以分析得清楚呢?
    我们就以这个简单的示例,分析 LinearLayout 中的 onMeasure 流程

    image1.png

    2.1.2 onMeasure() 执行流程

    在测量阶段,也就是 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 阶段,主要测量 LinearLayout 的整体大小,以及其中每个 childView 的大小

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

    onMeasure(int widthMeasureSpec, int heightMeasureSpec) 源码如上所示,通过 mOrientation 分别处理垂直和水平两个方向的测量,其中的 mOrientation 变量则是我们在 xml 布局文件中通过 android:orientation="vertical" 或者直接通过 setOrientation(@OrientationMode int orientation) 方法设置的 LinearLayout 文件方向变量

    我们仅分析垂直方向的测量方法,也就是 measureVertical(int widthMeasureSpec, int heightMeasureSpec)(水平方向的测量方法 measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) 是类似的原理,有兴趣的朋友可以自己分析)。measureVertical 方法还是很长的,不过整个过程可以分为三个阶段,为了分析的比较清楚,我们也分阶段循序渐进的分析

    1. 声明变量

    measureVertical 开始之前,需要初始化一些类变量 & 声明一些重要的局部变量,重要的变量我都有注释
    其中,最重要的就是有三类:

    1. mTotalLength:所有 childView 的高度和 + 本身的 padding,注意:它和 LinearLayout 本身的高度是不同的
    2. 三个宽度相关的变量
      • maxWidth:所有 childView 中宽度的最大值
      • alternativeMaxWidth:所有 layout_weight <= 0 的 childView 中宽度的最大值
      • weightedMaxWidth:所有 layout_weight >0 的 childView 中宽度的最大值
    3. totalWeight:所有 childView 的 weight 之和
        void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
            // 一些重要的变量
            mTotalLength = 0;            // 所有 childView 的高度和 + 本身的 padding,注意:它和 LinearLayout 本身的高度是不同的
            int maxWidth = 0;            // 所有 childView 中宽度的最大值
            int childState = 0;
            int alternativeMaxWidth = 0;    // 所有 layout_weight <= 0 的 childView 中宽度的最大值
            int weightedMaxWidth = 0;       // 所有 layout_weight >0 的 childView 中宽度的最大值
            boolean allFillParent = true;
            float totalWeight = 0;          // 所有 childView 的 weight 之和
    
            final int count = getChildCount();
    
            final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    
            boolean matchWidth = false;
            boolean skippedMeasure = false;    
    
            final int baselineChildIndex = mBaselineAlignedChildIndex;
            final boolean useLargestChild = mUseLargestChild;
    
            int largestChildHeight = Integer.MIN_VALUE;
            int consumedExcessSpace = 0;
    
            int nonSkippedChildCount = 0;
        }
    
    2. 测量第一阶段

    在测量第一阶段会计算那些没有设置 weight 的 childView 的高度、计算 mTotleLength,并且计算三个宽度相关的变量的值

    在看下面代码之前,请想想我们上面提到的 xml 布局是什么样的,我们就按照上面的 xml 布局文件的样式进行分析。其中一些重要的英文注释,我并没有去掉,大家可以仔细思考这些英文注释,有助于理解

        void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
            // 接上面的代码
    
            // See how tall everyone is. Also remember max width.
            // 第一次循环遍历,正如上面的英文注释所说明的意图所在
            for (int i = 0; i < count; ++i) {
                // 依次得到每一个 childView
                // { 在此 xml 布局中,会依次得到 TextView1 & TextView2 }
                final View child = getChildAt(index);
                // { 在此 xml 布局中的 TextView1 & TextView2 都不满足下面的两个条件 }
                if (child == null) {
                    continue;
                }
    
                if (child.getVisibility() == View.GONE) {
                   continue;
                }
    
                // 没有跳过的 childView 个数
                // { 在此 xml 布局中,nonSkippedChildCount 最终为 2 }
                nonSkippedChildCount++;
                // 在总高度中加上每一个 Divider 的 height
                // { 在此 xml 布局中,没有设置 `android:divider` 相关属性,跳过此 if 判断 }
                if (hasDividerBeforeChildAt(i)) {
                    mTotalLength += mDividerHeight;
                }
    
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                
                // 计算总权重 totalWeight
                // { 在此 xml 布局中,两个 childView 都没有设置 `android:layout_weight` 属性,
                // 所以 totalWeight 一直为 0} 
                totalWeight += lp.weight;
    
                // { 在此 xml 布局中,useExcessSpace 为 false }
                final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
                if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                    // 符合这种条件的 childView 先跳过测量,在这里不做测量计算
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                    skippedMeasure = true;
                } else {
                    if (useExcessSpace) {
                        // The heightMode is either UNSPECIFIED or AT_MOST, and
                        // this child is only laid out using excess space. Measure
                        // using WRAP_CONTENT so that we can find out the view's
                        // optimal height. We'll restore the original height of 0
                        // after measurement.
                        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).
                    // 这是非常重要的一个方法,将会决定每个 childView 的大小
                    // 如果此 childView 及在此 childView 之前的 childView 中使用了 weight 属性,
                    // 我们允许此 childView 使用所有的空间(后续如果需要,再做调整)
                    // { 在此 xml 布局中,在调用时 usedHeight 都是 mTotalLength }
                    final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
                    measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                            heightMeasureSpec, usedHeight);
                    
                    // 得到测量之后的 childView 的 childHeight
                    // { 在此 xml 中,TextView1 的 childHeight 是 200 dp;
                    // TextView2 的 childHeight 是 300 dp }
                    final int childHeight = child.getMeasuredHeight();
                    if (useExcessSpace) {
                        // Restore the original height and record how much space
                        // we've allocated to excess-only children so that we can
                        // match the behavior of EXACTLY measurement.
                        lp.height = 0;
                        consumedExcessSpace += childHeight;
                    }
    
                    // 将此 childView 的 childHeight 加入到 mTotalLength 中
                    // 并加上 childView 的 topMargin 和 bottomMargin 
                    // getNextLocationOffset 方法返回 0,方便以后扩展使用
                    // { 在此 xml 中,mTotalLength 最后的结果将是 500 dp }
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
                           lp.bottomMargin + getNextLocationOffset(child));
    
                    if (useLargestChild) {
                        largestChildHeight = Math.max(childHeight, largestChildHeight);
                    }
                }
    
                // 下面两个 if 判断都和 `android:baselineAlignedChildIndex` 属性有关
                // 在这里不做分析
                if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
                   mBaselineChildTop = mTotalLength;
                }
    
                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;
                // { 在此 xml 中,`android: layout_width` 是 `match_parent`,
                // 所以 widthMode 是 `MeasureSpec.EXACTLY`,不会进入此 if 判断  }
                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);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
    
                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 {
                    // { 在此 xml 布局中,最终都会走到此 代码块 中,matchWidthLocally == false }
                    alternativeMaxWidth = Math.max(alternativeMaxWidth,
                            matchWidthLocally ? margin : measuredWidth);
                }
    
                i += getChildrenSkipCount(child, i);
            }
            
            // 如果存在没有跳过的 childView 并且需要绘制 end divider 则需要加上 end 位置的 divider 的高度
            // { 在此 xml 中,没有设置 android:showDividers="end",跳过此 if 代码块 }
            if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
                mTotalLength += mDividerHeight;
            }
    
            ......
        }
    

    在上面的代码中,我都做了详细的注释,其中有一个方法调用非常重要,即 measureChildBeforeLayout() 方法,在此方法中将会计算每个 childView 的大小

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

    measureChildBeforeLayout() 方法中,又调用 ViewGroupmeasureChildWithMargins() 方法计算每个 childView 的大小,在测量垂直方向的 childView 时,有一个非常重要的参数需要注意,即:heightUsed,根据英文注释,heightUsed 是指在垂直方向,已经被 parentView 或者 parentView 的其他 childView 使用了的空间

        /**
         * Ask one of the children of this view to measure itself, taking into
         * account both the MeasureSpec requirements for this view and its padding
         * and margins. The child must have MarginLayoutParams The heavy lifting is
         * done in getChildMeasureSpec.
         *
         * @param child The child to measure
         * @param parentWidthMeasureSpec The width requirements for this view
         * @param widthUsed Extra space that has been used up by the parent
         *        horizontally (possibly by other children of the parent)
         * @param parentHeightMeasureSpec The height requirements for this view
         * @param heightUsed Extra space that has been used up by the parent
         *        vertically (possibly by other children of the parent)
         */
        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);
        }
    

    那么在上面示例的 xml 布局测量过程中 heightUsed 的值是多少呢?

    • 在测量 TextView1heightUsed0,因为是第一个测量的 childView,在垂直方向的空间还没有被使用
    • 在测量 TextView2heightUsed200 dp ,因为 TextView1 已经使用了 200 dp
    3. 测量第二阶段

    如果进入这个 if 条件,会进行第二次的 for 循环遍历 childView,重新计算 mTotalLength。不过这个 if 条件需要 useLargestChildtrueuseLargestChild 可以通过 xml 属性 android:measureWithLargestChild 设置的,不在本文的讨论范围内

        void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
            // 接上面的代码
    
            if (useLargestChild &&
                    (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
                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));
                }
            }
    
            ......
    
        }
    
    4. 测量第三阶段

    经过上面的分析之后,终于来到了最后的一个阶段,在这里会针对设置了 android:layout_weight 属性的布局,重新计算 mTotalLength

        void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
            // 接上面的代码
    
            // 加上 LinearLayout 自己的 paddingTop 和 paddingBottom
            mTotalLength += mPaddingTop + mPaddingBottom;
    
            int heightSize = mTotalLength;
    
            // 通过 getSuggestedMinimumHeight() 得到建议最小高度,并和计算得到的
            // mTotalLength 比较取最大值
            // { 在此 xml 布局中,并没有设置 minHeight 和 background,所以还是取 mTotalHeight 值}
            heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
            // 通过 heightMeasureSpec,调整 heightSize 的大小,具体的过程需要
            // 看一下 resolveSizeAndState() 方法的实现
            // {在此 xml 布局中,heightSize 经过调整之后就是 LinearLayout 的大小了,
            // 也就是整个屏幕的高度了 }
            int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
            heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
            
            // 重新计算有 weight 属性的 childView 大小,
            // 如果还有可用的空间,则扩展 childView,计算其大小
            // 如果 childView 超出了 LinearLayout 的边界,则收缩 childView
            // { 在此 xml 布局中,不会进入此 if 语句,直接走 else 代码块了,
            // 因为不符合条件,skippedMeasure == false,totalWeight == 0 }
            int remainingExcess = heightSize - mTotalLength
                    + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
            if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
                ......
            } else {
                // 重新计算 alternativeMaxWidth
                alternativeMaxWidth = Math.max(alternativeMaxWidth,
                                               weightedMaxWidth);
    
    
                // useLargestChild 为 false,不在本文讨论范围内
                if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
                    for (int i = 0; i < count; i++) {
                        final View child = getVirtualChildAt(i);
                        if (child == null || child.getVisibility() == View.GONE) {
                            continue;
                        }
    
                        final LinearLayout.LayoutParams lp =
                                (LinearLayout.LayoutParams) child.getLayoutParams();
    
                        float childExtra = lp.weight;
                        if (childExtra > 0) {
                            child.measure(
                                    MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
                                            MeasureSpec.EXACTLY),
                                    MeasureSpec.makeMeasureSpec(largestChildHeight,
                                            MeasureSpec.EXACTLY));
                        }
                    }
                }
            }
    
            if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
                maxWidth = alternativeMaxWidth;
            }
    
            maxWidth += mPaddingLeft + mPaddingRight;
    
            // 调整 width 大小
            maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
            // 调用 setMeasuredDimension() 设置 LinearLayout 的大小
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    heightSizeAndState);
    
            if (matchWidth) {
                forceUniformWidth(count, heightMeasureSpec);
            }
        }
    

    经过上面四步的源码分析,非 weight 情况下的垂直布局 onMeasure() 代码就分析的差不多了。在不使用 android:layout_weight 属性时,LinearLayout 的 onMeasure 流程还是比较简单的,只会进入第一个 for 循环遍历所有的 childView 并计算他们的大小,如果使用了 android:layout_weight 属性则会进入第三个 for 循环并再次遍历所有的 childView,再次重新执行 childView 的 measure() 方法

    2.2 使用 weight 的情况

    2.2.1 布局文件 & 效果

    上面分析了不使用 android:layout_weight 的情况,现在来分析下使用 android:layout_weight 的情况,还是通过一个例子入手,xml 布局如下所示

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/white"
            android:showDividers="end"
            android:orientation="vertical">
    
        <TextView
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:text="TextView1"
                android:gravity="center"
                android:textSize="24sp"
                android:layout_weight="2"
                android:textColor="@android:color/white"
                android:background="@android:color/holo_green_light"/>
    
        <TextView
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:text="TextView2"
                android:gravity="center"
                android:textSize="24sp"
                android:layout_weight="3"
                android:textColor="@android:color/white"
                android:background="@android:color/holo_blue_light"/>
    </LinearLayout>
    

    这也是一个我们最常见的 LinearLayout 的用法,TextView1 的 android:layout_height="0dp"android:layout_weight="2",TextView2 的 android:layout_height="0dp"android:layout_weight="3",如下图所示,TextView1 和 TextView2 在垂直方向上,会以 2 : 3 的比例分配整个屏幕的高度

    image2.png
    1. 声明变量

    还是和上面同样的思路分析 onMeasure 的流程,由于声明的变量没有区别,我们直接跳过声明变量,从测量第一阶段开始

    2. 测量第一阶段

    如果是上面这种布局的 xml 代码,在第一次 for 循环遍历 childView 时,会标记 skippedMeasure = true,并计算所有的 totalWeight,在第二次 for 循环遍历时,重新计算每个有 weight 属性的 childView 的大小

        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);
                // 遍历每个 childView,如果满足下面两个 if 条件之一,则跳过
                // { 在此 xml 布局中,两个 TextView 都不会跳过 }
                if (child == null) {
                    mTotalLength += measureNullChild(i);
                    continue;
                }
    
                if (child.getVisibility() == View.GONE) {
                   i += getChildrenSkipCount(child, i);
                   continue;
                }
    
                // 没有跳过的 childView 个数
                // { 在此 xml 布局中,nonSkippedChildCount 最终为 2 }
                nonSkippedChildCount++;
                // 在总高度中加上每一个 Divider 的 height
                // { 在此 xml 布局中,没有设置 `android:divider` 相关属性,跳过此 if 判断 }
                if (hasDividerBeforeChildAt(i)) {
                    mTotalLength += mDividerHeight;
                }
    
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            
                // 不同的地方开始了
                // 计算总权重 totalWeight
                // { 在此 xml 布局中,TextView1 的 weight == 2,TextView2 的 weight == 3 
                // 所以最终 totalWeight == 5 }  
                totalWeight += lp.weight;
    
                // {在此 xml 布局中,遍历 TextView1 和 TextView2 时,useExcessSpace 均为 true,
                // 并且满足下面的 if 条件判断,skippedMeasure 赋值为 true }
                final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
                if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
                    // 符合这种条件的 childView 先跳过这个循环测量,将 skippedMeasure 赋值为 true,
                    // 在后面第三个 for 循环重新计算此 childView 大小
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
                    skippedMeasure = true;
                } else {
                    ......
                }
    
                // 下面两个 if 判断都和 `android:baselineAlignedChildIndex` 属性有关
                // 在这里不做分析
                if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
                   mBaselineChildTop = mTotalLength;
                }
    
                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;
                // { 在此 xml 布局中,直到这里都还没有测量 childView,所以
                // child.getMeasuredWidth() == 0}
                final int measuredWidth = child.getMeasuredWidth() + margin;
                maxWidth = Math.max(maxWidth, measuredWidth);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
    
                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
                // { 在此 xml 布局中,weightedMaxWidth 一直为 0 }
                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);
            }
    
            // { 和上面的作用一样,在计算高度时,计算 endDivider 的高度 }
            if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
                mTotalLength += mDividerHeight;
            }
    
            ......
    
        }
    
    3. 测量第二阶段

    第二阶段的测量和上面提到的一样,都是和 android:measureWithLargestChild 属性设置相关的,不在本文的讨论范围之内

    4. 测量第三阶段

    在第三阶段的测量之中,针对设置了 android:layout_weight 属性的布局,重新计算 mTotalLength

        void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
            // 接上面的代码
    
            // 加上 LinearLayout 自己的 paddingTop 和 paddingBottom
            mTotalLength += mPaddingTop + mPaddingBottom;
            
            // { 在此 xml 布局中,经过上面的两次 for 循环之后 mTotalLength == 0 }
            int heightSize = mTotalLength;
    
            // 通过 getSuggestedMinimumHeight() 得到建议最小高度,并和计算得到的
            // mTotalLength 比较取最大值
            // { 在此 xml 布局中,并没有设置 minHeight 和 background,所以还是取 mTotalHeight 值,
            // 所以 heightSize == 0 }
            heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
    
            // 通过 heightMeasureSpec,调整 heightSize 的大小,具体的过程需要
            // 看一下 resolveSizeAndState() 方法的实现
            // { 在此 xml 布局中,heightSize 经过调整之后就是 LinearLayout 的大小了,
            // 也就是整个屏幕的高度了 }
            int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
            heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
            
            // 重新计算有 weight 属性的 childView 大小,
            // 如果还有可用的空间,则扩展 childView,计算其大小
            // 如果 childView 超出了 LinearLayout 的边界,则收缩 childView
            // { 在此 xml 布局中,经过上面的第一次  for 循环之后 skippedMeasure == true,
            // remainingExcess == 整个屏幕的高度,totalWeight == 5 }
            int remainingExcess = heightSize - mTotalLength
                    + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
            if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
                // 根据 mWeightSum 计算得到 remainingWeightSum,mWeightSum 是通过 
                // `android:weightSum` 属性设置的,totalWeight 是通过第一次 for 循环计算得到的
                float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
                // 将 mTotalLength 复位为 0
                mTotalLength = 0;
                // 开始真正的第二次 for 循环遍历每一个 childView,重新测量每一个 childView
                for (int i = 0; i < count; ++i) {
                    // 得到每一个 childView,如果符合下面的 if 判断则跳过
                    final View child = getVirtualChildAt(i);
                    if (child == null || child.getVisibility() == View.GONE) {
                        continue;
                    }
    
                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                    final float childWeight = lp.weight;
                    // 如果该 childView 设置了 `weight` 值,则进入 if 语句块
                    // { 在此 xml 布局中,TextView1 的 layout_weight == 2,
                    // TextView2 的 layout_weight == 3,都会进入下面的 if 条件判断 }
                    if (childWeight > 0) {
                        // 这是设置了 weight 的情况下,最重要的一行代码
                        // remainingExcess 剩余高度 * ( childView 的 weight / remainingWeightSum)
                        // share 便是此 childView 通过这个公式计算得到的高度,                               
                        // 并重新计算剩余高度 remainingExcess 和剩余权重总和 remainingWeightSum
                        final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
                        remainingExcess -= share;
                        remainingWeightSum -= childWeight;
    
                        // 通过下面的 if 条件重新计算,childHeight 是最终 childView 的真正高度            
                        // { 在此 xml 布局中,TextView1 和 TextView2 都会走到第二个条件中去,
                        // childHeight == share }
                        final int childHeight;
                        if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
                            childHeight = largestChildHeight;
                        } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
                                || heightMode == MeasureSpec.EXACTLY)) {
                            // This child needs to be laid out from scratch using
                            // only its share of excess space.
                            childHeight = share;
                        } else {
                            // This child had some intrinsic height to which we
                            // need to add its share of excess space.
                            childHeight = child.getMeasuredHeight() + share;
                        }
    
                        // 计算 childHeightMeasureSpec & childWidthMeasureSpec,并调用 child.measure() 方法
                        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                                Math.max(0, childHeight), MeasureSpec.EXACTLY);
                        final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
                                lp.width);
                        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
                        // Child may now not fit in vertical dimension.
                        childState = combineMeasuredStates(childState, child.getMeasuredState()
                                & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
                    }
    
                    // 重新计算 maxWidth & alternativeMaxWidth
                    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;
    
                    // 考虑 childView.topMargin & childView.bottomMargin,重新计算 mTotalLength
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
                            lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
                }
    
                // 完成 for 循环之后,加入 LinearLayout 本身的 mPaddingTop & mPaddingBottom
                mTotalLength += mPaddingTop + mPaddingBottom;
                // TODO: Should we recompute the heightSpec based on the new total length?
            } else {
                ......
            }
    
            if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
                maxWidth = alternativeMaxWidth;
            }
    
            maxWidth += mPaddingLeft + mPaddingRight;
    
            // 调整 width 大小
            maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
            // 调用 setMeasuredDimension() 设置 LinearLayout 的大小
            setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                    heightSizeAndState);
    
            if (matchWidth) {
                forceUniformWidth(count, heightMeasureSpec);
            }
        }
    

    三. 小结

    经过上面对两种情况的分析,其实 onMeasure 流程已经比较清晰了,简单总结一下,我们可以学习到以下几点

    1. LinearLayout 的设计者有意的对设置了 weight 和不设置 weight 的情况分别处理,通过 skippedMeasure 变量 & childView.height & childView.weight 区分,从上面我举的两个例子中就可以明显的感受到,两种测量流程分的还是比较详细清楚的
    2. 在 LinearLayout 中总共有 3 个 for 循环,分别处理不同的流程
      • 第一个 for 循环,只会在不使用 weight 属性时进入,并有可能会测量每个 childView 的大小
      • 第二个 for 循环,在使用 android:measureWithLargestChild 时才会进入,并且即使进入也不会调用 childView 的测量方法,只会更新 mTotalLength 变量
      • 第三个 for 循环,只会在使用 weight 属性时进入,并测量每个 childView 的大小
    3. 通过上面的分析,即使是使用了 android:layout_weight 属性,childView 也不会一定就测量两次,还需要看 android:layout_height 和 LinearLayout 的 heightMode 属性
    4. 通过上面的源码分析,熟悉巩固了 measureChildWithMargins(...)resolveSizeAndState(...)getChildMeasureSpec(...)setMeasuredDimension(...)等 Api,这些 Api 对于我们自定义控件还是非常重要的

    相关文章

      网友评论

        本文标题:Android 8.0 LinearLayout 源码解析

        本文链接:https://www.haomeiwen.com/subject/jwtwyqtx.html