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

来源:互联网 发布:大连海关数据分中心 编辑:程序博客网 时间:2024/05/01 12:45

   上文从源码角度分析了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。

3 1