从源码切入 透彻理解Android的weight属性

最近在看一本古董书《50 Android Hacks》,而书中开篇的第一个Hack就是”使用weight属性实现视图的居中显示“。

事实上weight是一个使用简单,但却又十分强大的属性。但关于其实现原理和使用细节我们却不一定真正深入的进行过理解。
今天我们就来由浅入深,从源码中去好好的研究研究这个东西。看看它有哪些可能被我们忽视的地方。

以上述书中的案例来说,它的需求很简单,请实现“让一个按钮居中显示,且占据屏幕一半的宽度”的效果。
要实现这个需求也许有很多方式,但最简单的肯定就是通过weight来实现,例如:

<?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:gravity="center" android:weightSum="1">

    <Button  android:id="@+id/button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.5" />
</LinearLayout>

然后我们成功得到如下效果:
从源码切入 透彻理解Android的weight属性_第1张图片

weightSum与layout_weight

这里就引出了最基本的使用知识点weightSum与layout_weight,正如weight最直观的翻译就可以理解为权重。
所以,实际上通过定义在LinearLayout中的weightSum与定义在子控件中的layout_weight属性相互配合,
就完美的实现了子控件按照指定比重在父视图中分配空间的效果,这通常也是我们对于weight属性最常见的使用方式。

weightSum与layout_weight之间的关系如下(至于为什么如此,我们稍后也将在源码中得以验证):

  • 当我们明确的在LinearLayout指定了weightSum属性的值的时候,系统将会使用我们指定的值。
  • 而当我们没有指定该属性时,weightSum最终的值则将是该视图中所有子控件设定的layout_weight的和。

这一切看上去十分简单清晰,所以初初接触weight这个东西的时候,很容易给我们造成这样的错觉,那就是:weight属性就是用来按比例指定控件长度的。
实际上真的是这么简单吗?当然不是这么简单。一定要避免这种误区!!

子控件的宽高最终究竟怎么样被确定?

我们都发现,在使用weight属性的时候,通常我们都将width或者height属性设置为了”0dp”。
其实稍微思考一下,我们就不难猜想到,虽然weight虽然通常被看做按比例分配控件宽高,但它很可能是配合宽高的属性来使用的,否则没必要多此一举,还设置个0dp。

我们可以通过代码来验证一下,这也引出了我们第二个关注的地方“子控件的宽高最终究竟怎么样被确定?”。

关于这个问题,在《50 Android Hack》一书中,给出了如下一个计算公式:

Button’s width + Button’s weight * Layout’s width / weightSum

但更加有趣的是,根据代码进行测试,你会发现这个公式并不正确
所以再次告诫自己,很多时候我们在看书或者文章、博客的时候,也不能盲从,如果有疑问,还得自己多思考和验证。
造成错误的情况实际也很正常,毕竟任何东西的作者也同样是人,只要是人,犯错在所难免;
而更常见的情况是,IT技术的更新日新月异,很可能在你看到某个东西的时候,它已经不适用最新的情况了。

我们对上述的计算公式,通过代码来进行验证:

public class MainActivity extends AppCompatActivity {

    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final int screenWidth = getResources().getDisplayMetrics().widthPixels;
        button = (Button) this.findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {                              
            Toast.makeText(MainActivity.this,screenWidth + "//" + button.getWidth(),Toast.LENGTH_LONG).show();
            }
        });

    }
}

运行程序,点击屏幕正中的按钮,得到如下的吐司:

我们发现输出的屏幕宽度是1080px,而button的宽度是540px。OK,我们套入公式:0dp(px) + 0.5 * 1080px / 1 = 540px;
这时候,你可能有话要说了。怎么不对了?怎么不对了?这不和公式完全匹配吗?

好吧,那么我们试着将布局文件中button的width值设置为100px。再次运行程序,得到如下结果:

这个时候,就出现差异了,因为套用公式的话:100px + 0.5 * 1080px / 1 = 640px;才对。
而根据打印的结果,如果我们去推测,可能会想:是不是button的width也被乘以权重了呢?
因为如果是:(100px * 0.5) + (0.5 * 1080px) / 1,那么就和输出结果对的上号了。

别急着下结论,我们继续验证,假设我们现在仍然将button的width保持在100px,但将weight值从0.5改为0.6:

这个时候,我们发现这与我们之前的推断的计算公式也出现偏差了?这个时候我们只能说:
从源码切入 透彻理解Android的weight属性_第2张图片

没办法,继续接着验证,因为毕竟我们要推断一个所谓的公式,实际就是通过足够数量的条件来总结出一个规律而已。
接着验证,我们发现将控件width恒定为100px,而改变layout_weight属性的值:

  • 当weight为0.7时,最终button的宽度为786。
  • 当weight为0.8时,最终button的宽度为884。
  • 当weight为0.9时,最终button的宽度为982。

这个时候,我们好像发现了一个计算规律,那就是button的最终实际宽度为:
(Button’s weight * Layout’s width / weightSum) + Button’s width * (weightSum - Button’s weight)。
对应于我们上面的三种测试情况,我们来套用这个计算式加以验证:

  • (0.7 * 1080 / 1) + 100 * (1 - 0.7) = 786px;
  • (0.8 * 1080 / 1) + 100 * (1 - 0.8) = 884px;
  • (0.9 * 1080 / 1) + 100 * (1 - 0.9) = 982px;

但为了加大测试范围,假设我们再将weightSum改为2,button的width设置为50px,weight设置为0.4。
那么:(0.4 * 1080 / 2) + 50 * (1 - 0.4) = 246px;
但是通过实际的测试我们会发现实际上得到的button的宽度是256px,出现了10px的偏差。
反思我们总结的公式,不难发现出现的偏差多半是因为button的width“50px”没有和weightSum产生联系。
如果是:(0.4 * 1080 / 2) + 50 * (1 - 0.4 / weightSum(这里就是2)) ,结果就正确了。
所以我们推断出最终正确的公式实际上应该是:

(Button’s weight * Layout’s width / weightSum) + Button’s width * (weightSum - Button’s weight / weightSum)

这时候,根据这个公式,再进行各种类似的测试,就基本都能吻合了。

但这所谓的公式并不是我们最终所关心的,我们感兴趣的是:在源码中,weight究竟是怎么被计算的?
究竟是怎么样的实现原理,让我们最终总结得到了体现上述规律的一个公式。

真的就是这样了吗?

不知道大家有没有注意到,即使我们在上一段中,经过各种测试,推断出了一个最终的计算公式。
但对于这个公式,我们加上了例如“应该”,“类似”,”基本”这样的修饰词。这样的词的出现,通常表达的一层含义就是:底气不足。
事实上确实是这样的,因为紧接着下面的一种情况就将打破我们之前的“幻想”。
假设,我们将布局文件修改如下:

<?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:weightSum="1">

    <Button  android:id="@+id/button1" android:layout_width="0px" android:layout_height="wrap_content" android:layout_weight="0.5" android:text="button1" />

    <Button  android:id="@+id/button2" android:layout_width="600px" android:layout_height="wrap_content" android:text="button2" />

    <Button  android:id="@+id/button3" android:layout_width="0px" android:layout_height="wrap_content" android:layout_weight="0.5" android:text="button3" />
</LinearLayout>

会得到的如下的运行结果:
从源码切入 透彻理解Android的weight属性_第3张图片

而如果布局文件是下面这种情况的话:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="200dp" android:layout_height="match_parent" android:weightSum="1" android:background="@color/colorAccent">

    <Button  android:id="@+id/button1" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.5" android:text="button1" />

    <Button  android:id="@+id/button2" android:layout_width="200dp" android:layout_height="wrap_content" android:text="button2" />

</LinearLayout>

你会发现我们的button会更夸张的直接消失了:
从源码切入 透彻理解Android的weight属性_第4张图片

这又带给了我们的新的疑问?连同之前的问题一起,我们带着这些疑问走进源码。

在源码里寻找答案

在源码中寻找答案,但起码我们首先得找到目的地。因为layout_weight属性在任何view控件里都能设置。
所以我们如果稍微一犯迷糊,很可能下意识的去View类中查看源码,然后结果当然是一无所获。
很明显的一点是,weight这个东西是配合LinearLayout作为父视图来使用的,所以它更应该属于LinearLayout类。

于是,我们打开LinearLayout类的源码进行研究,这里以最新的Android 6.0(即API 23)的源码为例。
如果我们对于自定义View或者View的绘制机制有些许的了解,就不难想象到,因为涉及view的测量,所以我们要找的代码多半存在于onMeasure方法当中。

打开LinearLayout类的onMeasure方法:

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

代码很简单,根据LinearLayout设置的排列方式,有两种不同的测量方法。
因为我们之前的用例,都是在水平排列的方式下进行的,所以我们这里就以measureHorizontal()为例来看看其原理。

打开measureHorizontal()方法,首先会看到一系列局部变量的定义。
当然这里涉及到的变量比较多,我们只挑我们这里关心的几个重要的变量记忆就OK了(加以注释)。

        // 测量得到的LinearLayout的总长度
        mTotalLength = 0;
        int maxHeight = 0;
        int childState = 0;
        int alternativeMaxHeight = 0;
        int weightedMaxHeight = 0;
        boolean allFillParent = true;
        float totalWeight = 0;
        // 子控件的个数
        final int count = getVirtualChildCount();
        // 宽高的测量模式
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        boolean matchHeight = false;
        boolean skippedMeasure = false;

        if (mMaxAscent == null || mMaxDescent == null) {
            mMaxAscent = new int[VERTICAL_GRAVITY_COUNT];
            mMaxDescent = new int[VERTICAL_GRAVITY_COUNT];
        }

        final int[] maxAscent = mMaxAscent;
        final int[] maxDescent = mMaxDescent;

        maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
        maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1;

        final boolean baselineAligned = mBaselineAligned;
        final boolean useLargestChild = mUseLargestChild;
        // 宽度的测量模式是否为MeasureSpec.EXACTLY(即精确测量)
        final boolean isExactly = widthMode == MeasureSpec.EXACTLY;

        int largestChildWidth = Integer.MIN_VALUE;

在一系列局部变量的定义之后,首先会进入如下的一个for循环:
(PS:下面这部分代码,个人理解为第一次测量过程。所谓的第一次测量,就是指所有的子控件都会参加,无论它是否设置了weight属性)

       // See how wide everyone is. Also remember max height.
        for (int i = 0; i < count; ++i) {
            //获取子控件
            final View child = getVirtualChildAt(i);
            //measureNullChild方法的返回值是0,所以这里的实际工作就是如果控件为null,那么layout的总长度不变
            if (child == null) {
                mTotalLength += measureNullChild(i);
                continue;
            }
            //道理类似,也就是如果子控件的可见性为gone,则跳过测量
            if (child.getVisibility() == GONE) {
                i += getChildrenSkipCount(child, i);
                continue;
            }
            //这个也很好理解,如果设置了分隔线,layout的长度需要加上divider-width
            if (hasDividerBeforeChildAt(i)) {
                mTotalLength += mDividerWidth;
            }
            // 获取子控件的layout params
            final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                    child.getLayoutParams();
            // 累加weight
            totalWeight += lp.weight;
            // 如果layout的测量模式为EXACTLY;子控件的宽度为0,且weight设置大于0
            if (widthMode == MeasureSpec.EXACTLY && lp.width == 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.
                // 就是测量layout的长度,加上子控件的左、右外边距
                if (isExactly) {
                    mTotalLength += lp.leftMargin + lp.rightMargin;
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength +
                            lp.leftMargin + lp.rightMargin);
                }

                // Baseline alignment requires to measure widgets to obtain the
                // baseline offset (in particular for TextViews). The following
                // defeats the optimization mentioned above. Allow the child to
                // use as much space as it wants because we can shrink things
                // later (and re-measure).
                if (baselineAligned) {
                    final int freeWidthSpec = MeasureSpec.makeSafeMeasureSpec(
                            MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED);
                    final int freeHeightSpec = MeasureSpec.makeSafeMeasureSpec(
                            MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED);
                    child.measure(freeWidthSpec, freeHeightSpec);
                } else {
                    skippedMeasure = true;
                }
            } else {
                int oldWidth = Integer.MIN_VALUE;

                if (lp.width == 0 && lp.weight > 0) {
                    // widthMode 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 width of 0

                    // 简单的理解就是如果测量模式是UNSPECIFIED或者AT_MOST
                    // 而且子控件的width值设置为0,而weight值设置大于0
                    // 则将width的值先设定为WRAP_CONTENT
                    oldWidth = 0;
                    lp.width = 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).

                // 这里是很关键的一个方法,顾名思义,在执行onLayout之前,先完成子控件的测量。
                measureChildBeforeLayout(child, i, widthMeasureSpec,
                        totalWeight == 0 ? mTotalLength : 0,
                        heightMeasureSpec, 0);

                if (oldWidth != Integer.MIN_VALUE) {
                    lp.width = oldWidth;
                }
                // 获取子控件的测量宽度
                final int childWidth = child.getMeasuredWidth();
                // 改变layout的总长度(控件宽度+左右外边距)
                if (isExactly) {
                    mTotalLength += childWidth + lp.leftMargin + lp.rightMargin +
                            getNextLocationOffset(child);
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin +
                           lp.rightMargin + getNextLocationOffset(child));
                }

                if (useLargestChild) {
                    largestChildWidth = Math.max(childWidth, largestChildWidth);
                }
            }

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

            final int margin = lp.topMargin + lp.bottomMargin;
            final int childHeight = child.getMeasuredHeight() + margin;
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (baselineAligned) {
                final int childBaseline = child.getBaseline();
                if (childBaseline != -1) {
                    // Translates the child's vertical gravity into an index
                    // in the range 0..VERTICAL_GRAVITY_COUNT
                    final int gravity = (lp.gravity < 0 ? mGravity : lp.gravity)
                            & Gravity.VERTICAL_GRAVITY_MASK;
                    final int index = ((gravity >> Gravity.AXIS_Y_SHIFT)
                            & ~Gravity.AXIS_SPECIFIED) >> 1;

                    maxAscent[index] = Math.max(maxAscent[index], childBaseline);
                    maxDescent[index] = Math.max(maxDescent[index], childHeight - childBaseline);
                }
            }

            maxHeight = Math.max(maxHeight, childHeight);

            allFillParent = allFillParent && lp.height == LayoutParams.MATCH_PARENT;
            if (lp.weight > 0) {
                /* * Heights of weighted Views are bogus if we end up * remeasuring, so keep them separate. */
                weightedMaxHeight = Math.max(weightedMaxHeight,
                        matchHeightLocally ? margin : childHeight);
            } else {
                alternativeMaxHeight = Math.max(alternativeMaxHeight,
                        matchHeightLocally ? margin : childHeight);
            }

            i += getChildrenSkipCount(child, i);
        }

同理,代码仍然比较复杂也比较多,所以我们只针对关键的部分加以注释和理解。
而我们对涉及到解答我们之前提出的几点疑问的几行关键代码再单独提出来加强印象:
首先是第20 - 23行的代码:

            // 获取子控件的layout params
            final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
                    child.getLayoutParams();
            // 累加weight
            totalWeight += lp.weight;

我们看到,这里会循环遍历得到所有子控件的weight值,并进行累加计算赋值给totalWeight变量。
OK,还记得吗?我们之前说的,如果没有设置weightSum的值,weightSum就等于所有子控件设置的weight的和。

接下来,需要注意的是一个if-else判断:

 if (widthMode == MeasureSpec.EXACTLY && lp.width == 0 && lp.weight > 0) {
  //..................
 }else{
  //..................
 }

也就是说,只有当“Layout的宽度测量模式为EXACTLY;子控件宽度为0,且weight设置大于0”三个条件都满足时,才会执行if代码块。比如:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:weightSum="1">

    <Button  android:id="@+id/button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.5" />
</LinearLayout>

而这种情况,需要做的工作很简单,if代码块会先忽视button的测量。因为现在系统也不清楚button最终的宽度为是多少。
它只是根据button的左、右外边距来修改layout的内容宽度“mTotalLength += lp.leftMargin + lp.rightMargin;”。

但实际上这样说也些虎断,因为在水平排列模式下,我们也而不能说是跳过了子控件的测量。
因为我们还需要到注意到,第43行到51行的一段代码,在这段代码中我们发现:
当满足baselineAligned为true,实际就是LinearLayout的baselineAligned属性为true时,其实还是进行了子控件的测量。
但我们发现,这里为其设定的测量模式是“MeasureSpec.UNSPECIFIED”。同时通过注释我们也可以明白,简单来说:
这里之所以进行一次测量,是因为基线对齐需要测量子部件以获得基线偏移。但实际上这个测量影响还好,因为我们之后还可以进行仔细的测量。

当以上任意条件不能够被满足,就会进入到else代码块,这个时候,特别需要我们注意的一个方法调用的代码就出现了:

 measureChildBeforeLayout(child, i, widthMeasureSpec, totalWeight == 0 ? mTotalLength : 0, heightMeasureSpec, 0);

通过这里,我们可以看到,如果说我们实际上已经为控件指定了一个具体的”width”的话,那么系统会进行一次该子控件的测量工作。

好了,在上面的测量过程之后。接下来我们看下一段关键部分的代码:
(PS:有一就有二,所以对应来说,个人理解这一部分的代码是第二次测量过程,与之前最大的不同在于,这次测量只有那些weight被设置为大于0的子控件参加)

       // layout的最终测量长度需要加上layout的内边距
        mTotalLength += mPaddingLeft + mPaddingRight;
        int widthSize = mTotalLength;

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

        // Reconcile our calculated size with the widthMeasureSpec
        int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
        widthSize = widthSizeAndState & MEASURED_SIZE_MASK;

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds. If we skipped
        // measurement on any children, we need to measure them now.
        // 获取delta
        int delta = widthSize - mTotalLength;
        if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {
            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

            maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
            maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1;
            maxHeight = -1;

            mTotalLength = 0;

            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 said it could absorb extra space -- give him his share
                    int share = (int) (childExtra * delta / weightSum);
                    weightSum -= childExtra;
                    delta -= share;

                    final int childHeightMeasureSpec = getChildMeasureSpec(
                            heightMeasureSpec,
                            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin,
                            lp.height);

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

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

                    // Child may now not fit in horizontal dimension.
                    childState = combineMeasuredStates(childState,
                            child.getMeasuredState() & MEASURED_STATE_MASK);
                }

                if (isExactly) {
                    mTotalLength += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin +
                            getNextLocationOffset(child);
                } else {
                    final int totalLength = mTotalLength;
                    mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredWidth() +
                            lp.leftMargin + lp.rightMargin + getNextLocationOffset(child));
                }

                boolean matchHeightLocally = heightMode != MeasureSpec.EXACTLY &&
                        lp.height == LayoutParams.MATCH_PARENT;

                final int margin = lp.topMargin + lp .bottomMargin;
                int childHeight = child.getMeasuredHeight() + margin;
                maxHeight = Math.max(maxHeight, childHeight);
                alternativeMaxHeight = Math.max(alternativeMaxHeight,
                        matchHeightLocally ? margin : childHeight);

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

                if (baselineAligned) {
                    final int childBaseline = child.getBaseline();
                    if (childBaseline != -1) {
                        // Translates the child's vertical gravity into an index in the range 0..2
                        final int gravity = (lp.gravity < 0 ? mGravity : lp.gravity)
                                & Gravity.VERTICAL_GRAVITY_MASK;
                        final int index = ((gravity >> Gravity.AXIS_Y_SHIFT)
                                & ~Gravity.AXIS_SPECIFIED) >> 1;

                        maxAscent[index] = Math.max(maxAscent[index], childBaseline);
                        maxDescent[index] = Math.max(maxDescent[index],
                                childHeight - childBaseline);
                    }
                }
            }

            // Add in our padding
            mTotalLength += mPaddingLeft + mPaddingRight;
            // TODO: Should we update widthSize with the new total length?

            // Check mMaxAscent[INDEX_TOP] first because it maps to Gravity.TOP,
            // the most common case
            if (maxAscent[INDEX_TOP] != -1 ||
                    maxAscent[INDEX_CENTER_VERTICAL] != -1 ||
                    maxAscent[INDEX_BOTTOM] != -1 ||
                    maxAscent[INDEX_FILL] != -1) {
                final int ascent = Math.max(maxAscent[INDEX_FILL],
                        Math.max(maxAscent[INDEX_CENTER_VERTICAL],
                        Math.max(maxAscent[INDEX_TOP], maxAscent[INDEX_BOTTOM])));
                final int descent = Math.max(maxDescent[INDEX_FILL],
                        Math.max(maxDescent[INDEX_CENTER_VERTICAL],
                        Math.max(maxDescent[INDEX_TOP], maxDescent[INDEX_BOTTOM])));
                maxHeight = Math.max(maxHeight, ascent + descent);
            }
        } else {
            alternativeMaxHeight = Math.max(alternativeMaxHeight, weightedMaxHeight);

            // We have no limit, so make all weighted views as wide as the largest child.
            // Children will have already been measured once.
            if (useLargestChild && widthMode != 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(largestChildWidth, MeasureSpec.EXACTLY),
                                MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
                                        MeasureSpec.EXACTLY));
                    }
                }
            }
        }

在上述代码中,我们首先要关心的是第16行的代码:

int delta = widthSize - mTotalLength;

这里是获取了一个差值delta,这个差值我们可以理解为是LinearLayout的宽度减去第一次测量工作完成后得到的内容宽度(mTotalLength)。
这个差值实际上是很关键的,因为关于weight的实际运算工作就和这个值紧密相关。
我们看到在上面这行代码之后,紧接着就是一行if判断:

if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {

也就是说:如果skippedMeasure为true(这种情况发生在LinearLayout的baselineAligned属性为false) ;
或者满足delta!=0并且之前计算得到的totalWeight的值大于0时,才会进行子控件的weight的计算工作。

delta小于0,实际代表现有的内容宽度已经超过了layout自身的宽度;而大于0则代表还有空余的空间。
当delta等于0,则代表现在测量过后,内容的宽度恰恰填满layout,所以不进行多余的计算了。

在这个if语句块里的第一行代码就是:

            float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;

你看到了,weightSum的计算,熟悉的家伙。

而关于针对于weight计算控件最终宽度的原理,实际发生在第36行到66行代码之间,即:

 float childExtra = lp.weight;
                // 只有当子控件的weight设置大于0,才需要根据weight计算宽度(这也是为什么前面说这次测量过程只有weight设置为大于0的控件参加)
                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 childHeightMeasureSpec = getChildMeasureSpec(
                            heightMeasureSpec,
                            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin,
                            lp.height);

                    // TODO: Use a field like lp.isMeasured to figure out if this
                    // 如果width不等于0,或者不是EXACTLY测量模式(实际上就是对应之前的测量过程中,已经执行过measureChildBeforeLayout的控件)
                    if ((lp.width != 0) || (widthMode != MeasureSpec.EXACTLY)) {
                        // child was measured once already above ... base new measurement
                        // 最红它的实际宽度将是指定的width加上share的和
                        int childWidth = child.getMeasuredWidth() + share;
                        if (childWidth < 0) {
                            childWidth = 0;
                        }

                        child.measure(
                            MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
                            childHeightMeasureSpec);
                    } else {
                        // 如果是首次测量(其实就是针对那些width = 0,weight>0的控件,宽度就是计算得到的share)
                        child.measure(MeasureSpec.makeMeasureSpec(
                                share > 0 ? share : 0, MeasureSpec.EXACTLY),
                                childHeightMeasureSpec);
                    }

到了这里,我们实际上就对weight属性有了一个不错的理解了。
不知道你总结出没有,我们之前提出的疑问,其实都都已经一一在源码中找到了答案。我们再对应来看一下。

  • 首先,是我们说到的weightSum与layout-weight之间的关系。

形成我们所说的关系的原因很简单,就两句关键的代码:

totalWeight += lp.weight;
//以及
float weightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
  • 接着,是子控件的宽高最终究竟怎么样被确定?

你还记得我们之前自作聪明的根据测试结果进行推断计算公式吗?实际上子控件的宽高最终的决定方式很简单:

1、那些满足widthMode == MeasureSpec.EXACTLY && lp.width == 0 && lp.weight > 0的控件,在第一次测量过程中可以视为并没有被真正测量宽高。

2、对于明确指定了宽、高的值的控件,在第一次测量过程中也会先进行一次测量,这个时候测量的值就是我们明确指定的值。

3、在第一次测量过程之后,第二次测量过程开始之前,会计算得到一个layout的现有内容占据的宽度mTotalLength,这个宽度是:所有子控件的左、右(上、下)外边距 + 被明确赋值的控件的宽、高 + layout的内边距

4、对于weight的计算,如果控件的宽高设置的是0,它们所分配到的实际的范围其实是变量share的值,即”子控件的weight * layout剩余的空间 / weightSum;如果不为0,则是share+明确设定的宽、高值”。

这个时候,我们再以“layout宽为1080px,weightSum为1,button的width为100px,weight为0.5”来说,
其实我们就不用再去套什么所谓的公式了,通过源码的理解,我们能够轻松描述它的计算过程。
因为明确的设置了width为100px,所以在第一次测量过程中,button就被设置为了100px。
当第一次测量完成,得到的mTotalLength实际上也就是100px,因为我们的layout里就只有一个button,
并且button并没有设置外边距(margin),layout也没有设置内边距(padding)。
所以这个时候对于进行第二次测量过程,delta计算后得到的值实际上是:1080px - 100px = 980px。
而share的计算则是:0.5 * 980px / 1 = 490px;而因为该button明确指定了不为0的width值,所以其最终的实际宽度是:
share + button.getMeasureWidth = 490 + 100 =590px。

同理来说,有button1,button2,button3三个按钮的例子当中,出现那样的情况,也是因为button2首先占据了600px的实际宽度。
所以button1,button3所谓的0.5的权重分享的,就只是剩下的480px的实际空间了。

而在最后一个例子里,我们看到的button1根本不在屏幕出现,很容易想到是因为在第一次测量后。
delta的计算结果将会是0,所以根本不会再进行额外的运算去为button1分配空间了。

你可能感兴趣的:(android,weight,源码剖析)