从源码角度分析linearLayout测量过程以及weight机制

   上文从源码角度分析了view和viewGroup的measure机制,如果还没有阅读的同志们,可以前往从源码角度分析Android View的绘制机制(一)阅读。下面我再结合linearLayout的measure过程解释以下两个问题的缘由。

问题一
<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:id="@+id/tv_1"
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:background="#aaffdd"
        android:layout_weight="2" />

    <TextView
        android:id="@+id/tv_2"
        android:layout_width="match_parent"
        android:layout_height="0dip"
        android:background="#ddaabb"
        android:layout_weight="4" />

</LinearLayout>

问题一显示结果:
这里写图片描述

问题二
<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:id="@+id/tv_1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#aaffdd"
        android:layout_weight="2" />

    <TextView
        android:id="@+id/tv_2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ddaabb"
        android:layout_weight="4" />

</LinearLayout>

问题二显示结果:
这里写图片描述

问题三
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/ll"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/tv_1"
        android:layout_width="match_parent"
        android:layout_height="500dip"
        android:background="#aaffdd"
        android:layout_weight="2" />

    <TextView
        android:id="@+id/tv_2"
        android:layout_width="match_parent"
        android:layout_height="100dip"
        android:background="#ddaabb"
        android:layout_weight="4" />

</LinearLayout>

问题三显示结果:
这里写图片描述

对于以上问题我们逐一分析。
问题一:
   parent的layout_height是match_parent,而两个TextView的layout_height都是0并且layout_weight分别是2和4,但是显示出来的高度比例是1:2。

     for (int i = 0; i < count; ++i) {
        ........
            //拿到child的LayoutParams 
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
            //将weight的值加到totalWeight,weight的值就是xml文件中的layout_weight属性的值  
            totalWeight += lp.weight;
            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
                 /** 
                  如果父View的mode是EXACTLY,并且height==0 并且lp.weight>0(就是我们上面的例子中的第一张图的情况) 
                  那么就先不measure这个child,直接把topMargin和bottoMargin等属性加到totaoLength中 
                */  
                final int totalLength = mTotalLength;
                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            } else {
                int oldHeight = Integer.MIN_VALUE;
                //如果父View不是EXACLTY,那么将子View的height变为WRAP_CONTENT  
                if (lp.height == 0 && lp.weight > 0) {
                    oldHeight = 0;
                    lp.height = LayoutParams.WRAP_CONTENT;
                }
                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);
                }
            }
            .............
            // 给总高度加上自身的padding
        mTotalLength += mPaddingTop + mPaddingBottom;
        //将所有View的高度赋值给heightSize
        int heightSize = mTotalLength;

        // Check against our minimum height
        // 最小的高度就是背景的高度mBGDrawable.getMinimumHeight()
        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
         //这里对heightSize再次赋值,不过如果LinearLayout是xml文件的根标签,并且设置到Activity的话  
        //此时heightSize的大小就是屏幕的高度,我们暂时就考虑等于屏幕高度的情况,其他情况类似
        // Reconcile our calculated size with the heightMeasureSpec
        // 如果heightMeasureSpec的size是精确的,那么这个heightsize值==heightMeasureSpec的size,也就是屏幕的高度
        heightSize = resolveSize(heightSize, heightMeasureSpec);

        // Either expand children with weight to take up available space or
        // shrink them if they extend beyond our current bounds
        //屏幕的高度还剩下delta,如果对于我们上面第一张图,delta>0,对于第二张图则<0 
        int delta = heightSize - mTotalLength;
        if (delta != 0 && totalWeight > 0.0f) {
            //如果设置了weightsum属性,这weightSum等于weightsum的属性,否则等于totalWeight  
            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
                    // 计算方式:子view的weight属性值 * 剩余高度 / weight总和
                    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
                        // 重新设置子view的高度
                        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 (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0)的条件,所以两个TextView都不会测量大小只会先记录weight,然后看一下下面梁行代码:

int heightSize = mTotalLength;
......
heightSize = resolveSize(heightSize, heightMeasureSpec);
int delta = heightSize - mTotalLength;

这里mTotalLength其实是等于0的,因为两个TextView的高度都没有测量。通过调用resolveSize方法,参数heightSize为0,heightMeasureSpec的mode为MeasureSpec.EXACTLY ,所以这个方法返回的heightMeasureSpec的size,在这个例子中也就是linearLayout的height,也就是matchParent窗体的大小。剩余高度delta也就为窗体的大小。到目前为止两个TextView的高度都是0.

int share = (int) (childExtra * delta / weightSum);
weightSum -= childExtra;
delta -= share;
........
if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {
     // 重新设置子view的高度
     int childHeight = child.getMeasuredHeight() + share;
     if (childHeight < 0) {
            childHeight = 0;
     }
     child.measure(childWidthMeasureSpec,MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
 } else {    
     child.measure(childWidthMeasureSpec,MeasureSpec.makeMeasureSpec(share > 0 ? share :  MeasureSpec.EXACTLY));
 }

假设现在窗体的高度为900:
   第一个textView:share = 权重 2 * 900 / 总权重6 = 300。那么childHeight = 0 + 300 = 300,然后调用child的measure方法测量
   第二个textview:share = 权重 4 * (900 - 300) / 总权重(6-2) = 600。那么childHeight = 0 + 600 = 600,然后调用child的measure方法测量.
由于两个TextView的高度都是精确值,所以测量出来的分别是300、600,高度也就是1:2。

问题二:
linearlayout和两个TextView的高度都是match_Parent。
总权重还是2+4 = 6不变,但是过程中明显不满足if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0)这个条件,所以直接测量这个子view,走的是下面代码:

measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0);
..........
heightSize = resolveSize(heightSize, heightMeasureSpec);
int delta = heightSize - mTotalLength;
..........

这个方法里面,如果parentHeightMeasureSpec和mode是MeasureSpec.EXACTLY并且child的高度是大于0的话,测量出来的子view的高度就是子view本身layout_height,本例也就是match_parent,窗体的高度。
   假设窗体的高度为900,两个textview测量出来的高度总和为两个窗体的高度 2 *900 = 1800。上面也讲到resolveSize出来的heightSize 为900。所以剩余高度delta = 900 - 1800 = -900。
   第一个textView:share = 权重 2 * (-900) / 总权重6 = -300。那么childHeight = 900 + (-300) = 600,然后调用child的measure方法测量
   第二个textview:share = 权重 4 * (- 900 - (-300)) / 总权重(6-2) = -600。那么childHeight = 900 + (-600) = 300,然后调用child的measure方法测量.
由于两个TextView的高度都是精确值,所以测量出来的分别是600、300,高度也就是2:1。

问题三:
   linearlayout高度match_Parent,两个TextView的高度分别是500dip和200dip。总权重还是2+4 = 6不变,
   过程中明显不满足if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0)这个条件,所以直接测量这个子view,走的是下面代码:

measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0);
..........
heightSize = resolveSize(heightSize, heightMeasureSpec);
int delta = heightSize - mTotalLength;
..........

这个方法里面,如果parentHeightMeasureSpec和mode是MeasureSpec.EXACTLY并且child的高度是大于0的话,测量出来的子view的高度就是子view本身layout_height,对于第一个textView来讲就是500dip,第二个也就是200dip。
   我的手机上窗体的高度为1557px,密度为3.0,所以把dip换算成px后,第一个textView的测量高度为500 * 3.0 + 0.5f = 1500.5px。第二个textview的测量高度为200 * 3.0 + 0.5f = 600.5px。所以测量出来的总高度 = 1500.5 + 600.5 = 2101
   上面也讲到如果heightMeasureSpec的mode为MeasureSpec.EXACTLY ,所以这个方法返回的heightMeasureSpec的size,也就是1557px。所以剩余高度delta = 1557- 2101= -544。
   第一个textView:share = 权重 2 * (-544) / 总权重6 = -181。那么childHeight = 1500.5 + (-181) = 1319,然后调用child的measure方法测量。
   第二个textview:share = 权重 4 * (- 544 - (-181)) / 总权重(6-2) = -363。那么childHeight = 600.5 + (-363) = 238,然后调用child的measure方法测量.。
由于两个TextView的高度都是精确值,所以测量出来的分别是1319、238,高度也就不详之前一样的比值了。
   measure过程已经讲述完毕了。请继续关注本文下半部分view的绘制机制之layout和draw。

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值