尊重原创:http://blog.csdn.net/yuanzeyao/article/details/46842891
本篇文章接着上篇文章的内容来继续讨论View的绘制机制,上篇文章中我们主要讲解了View的measure过程,今天我们就来学习ViewGroup的measure过程,由于ViewGroup只是一个抽象类,所以我们需要以一个具体的布局来分析measure过程,正如我上篇文章说的,我打算使用LinearLayout为例讲解measure过程,如果你还没有读过上篇文章,那么建议你先浏览一下上篇文章吧:Android中View的绘制机制源码分析 一
在进行今天的主题之前,我来给大家分享一下我最近看到并且非常喜欢的两句话吧:
1、把生命浪费在美好的事物上
2、应该有一份不以此为生的职业
喜欢第一句话的原因是因为里面包含了一种乐观的生活态度,只要一件事情你在进行的过程中能够给你带来快乐,那么我们就值得花时间做,喜欢第二句话的原因是作为程序员这个职业以后转型的问题也是值得我们考虑的,相信大家也都听说过程序员是吃青春饭的职业,当你到35-40岁已经年老色衰的时候,你不得不考虑转型了,有部分转型为管理人才,有些人完全转型,干着和IT毫无关系的职业,所以我们是不是现在就要想想我们有没有一份不以此为生的职业呢?好吧 扯淡就扯到这里吧,下面我们步入正题。
我们来分析今天的第一个问题:你对layout_weight属性知多少?
相信大多数同学会说这个属性就是标明一个View在父View中所占的权重(比例),这个解释对吗?我们暂且不做评论,我们使用两个例子来验证一下:
example 1:
<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="0dip" android:layout_weight="2" android:text="New Text" android:background="#998877" android:id="@+id/textView" /> <TextView android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="4" android:text="New Text" android:background="#334455" android:id="@+id/textView2" /></LinearLayout>
效果图如下:
example 2:
<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="match_parent" android:layout_weight="2" android:text="New Text" android:background="#998877" android:id="@+id/textView" /> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_weight="4" android:text="New Text" android:background="#334455" android:id="@+id/textView2" /></LinearLayout>
效果图如下:在第一张图片中,上面的TextView的weidht是2,下面的TextView的weight是4,所以上面的TextView的高度是下面TextView高度的一半,注意此时两个TextView的layout_height都是0dip,再看下面的一张图,同样上面的TextView和下面TextView的weight分别是2和4,唯一不同的是它们的layout_height变为了match_parent,此时上面的高度确实下面的两倍
所以从第一张图片看来,layout_weight好像是代表比例的,但是从第二张图片看,刚好是相反的,我们今天就带着这个疑问开始分析LinearLayout的measure源码吧
LinearLayout的measuer调用的是View中的measure方法,从上篇文章中我们知道measure会调用onMeasure方法,所以直接从LinearLayout的onMeasure开始分析:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
看了源码是不是觉得so easy!,在onMeasure主要根据当前的LinearLayout是横向还是纵向,分别调用measureVertical方法和measureHorizontal方法,这里我们以纵向为例,看看measureVertical代码,由于代码比较长,我们分段分析:
Section one:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { //用来存储所有的子View使用的高度 mTotalLength = 0; int maxWidth = 0; int alternativeMaxWidth = 0; int weightedMaxWidth = 0; boolean allFillParent = true; //所有View的weight的和 float totalWeight = 0; //获得子View的个数 final int count = getVirtualChildCount(); //widthMeasureSpec和heightMeasureSpec就是父View传递进来的,这里拿到父View的mode final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
这里定义了几个重要的变量,mTotalLength,用来存储所有子View的高度,count存在子View的个数,widthMode和heightMode用来存储父View的mode(如果对于mode不熟,我以看我前面的一篇文章)。
Section Two:
//遍历所有的子View,获取所有子View的总高度,并对每个子View进行measure操作 for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { //如果child 是Null,则mTotalLength加0 mTotalLength += measureNullChild(i); continue; } if (child.getVisibility() == View.GONE) { //如果child不可见,则跳过 i += getChildrenSkipCount(child, i); continue; } //拿到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) { // 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); } }
这段代码是整个measureVertical最核心的部分了,我已经在代码中加入了相应的注释,这里我只想说的是为什么child.getLayoutParam能够直接强制转换为LinearLayout.LayoutParams,这个问题我们先保留吧,我打算后面的文章中专门分析一下这个LayoutParams这个对象。
我们发现在measureVertical中调用了一个measureChildBeforeLayout方法,我们先看看它传入的几个参数,我们发现最后一个参数听奇怪的,totalWeight==0?mTotalLength:0,也就是说对于一个View,如果这个View之前的View没有设置过layout_weight属性,那么这个参数等于mTotalLength,如果有设置过,那么传0,我们先进入measureChildBeforeLayout方法看看:
void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); }
其实就是调用父类ViewGroup的measureChildWidthMargins方法,这个方法我们在前篇文章已经分析过了,这里我们就不分析了,它就是对子View进行measure方法,只不过我们这里需要注意,如果前面有View设置了layout_weight属性,那么这里的totalHeight就是0,在执行完了measureChildBeforeLayout方法后,child的高度就知道了,就将child的高度累加到mTotalHeight中。
Section Three:
//将所有View的高度赋值给heightSize; int heightSize = mTotalLength; heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); //这里对heightSize再次赋值,不过如果LinearLayout是xml文件的根标签,并且设置到Activity的话 //此时heightSize的大小就是屏幕的高度,我们暂时就考虑等于屏幕高度的情况,其他情况类似 heightSize = resolveSize(heightSize, heightMeasureSpec); //屏幕的高度还剩下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; //重新遍历所有的子View for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); //如果子View不可见,直接跳过 if (child.getVisibility() == View.GONE) { continue; } LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); float childExtra = lp.weight; //如果设置了weight属性 if (childExtra > 0) { // Child said it could absorb extra space -- give him his share //从delta中分到(weight/weightSum)*delta,注意这里delta可能<0 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)) { /** 记得heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0吗 这个是Section Two的一个判断条件,也就是说如果走到这里,说明这个View前面已经measure过 现在要将share的值加入到高度上,所以要重新measure */ int childHeight = child.getMeasuredHeight() + share; if (childHeight < 0) { childHeight = 0; } child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); } else { /** 由于走到Section Two中走到heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0时,是直接跳过的 所以没有测量过,所以在这里对View进行测量 */ 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()); //所有的孩子View测量完毕,为自己设置大小 setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize);
这段代码主要是来重新绘制设置有layout_weight属性的子View,首先计算LinearLayout能够提供的高度大小heightSize,正如注释里面说的,在上面的两个例子中,heightSize都是屏幕的高度,然后通过heightSize和mTotalLenght计算还剩下的高度,然后将这些高度按照weight的比例分配给相应的View,然后调用View的measure方法,我们现在来解释上面的两个例子吧:
第一个例子:两个TextView的高度都是0dip,layout_weight分别是2 和 4,LinearLayout的mode=EXACTLY
从Section Two开始,条件满足heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 所以在SectionTwo执行完后两个TextView是没有执行measure的,所以mTotalLenght等于0。
进入Section Three,此时heightSize等于屏幕的高度,所以delta=heightSize-mTotalLenght=屏幕高度。weightSum=2+4=6.在遍历子View的时候,通过计算第一个TextView的高度是:屏幕高度*(2/6),并且delta=delta-屏幕高度*(2/6).weightSum=6-2=4.
由于第一个TextView不满足条件(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY),所以执行else里面的逻辑:
child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(share > 0 ? share : 0, MeasureSpec.EXACTLY));
所以第一个TextView的高度就是屏幕的1/3. 遍历完第一个TextView之后,遍历第二个TextView,同样的道理第二个 TextView的高度等于delta*(4/4),也就是等于delta的值,其实也就是 屏幕高度*(4/6)。
第二个例子:两个TextView的高度都是match_parent,layout_weight分别是2和4 ,LinearLayout的mode=EXACTLY
从Section Two开始,条件不满足heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0 ,所以执行到了else里面的逻辑
measureChildBeforeLayout( child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0);
此时totalWeight明显不等0,所以measureChildBeforeLayout最后一个参数明显是0,所以导致第一个View的高度绘制出来及时heightMeasureSpec的size,也就是屏幕的高度(原因见我上篇文章的分析)。同样的道理对于第二个TextView测量后高度也是整个屏幕的高度。所以导致这里算出的delta=(-屏幕的高度),也就是说是个负数,进入Section Three,很明显满足了(lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)这个条件,所以执行如下代码:
int childHeight = child.getMeasuredHeight() + share; if (childHeight < 0) { childHeight = 0; } child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
通过Section Two的分析child.getMeasureHeight等于屏幕高度,share=-屏幕高度*(2/6),也就是说第一个TextView的高度变为 屏幕的高度*(4/6),同样的道理可以得出第二个TextView的高度 屏幕的高度*(2/6)。
对于layout_weight属性的理解应该是这样的:在SectionTwo中测量完所有的View后,将delta的值按照weight的比例给相应的 View,如果delta>0,那么那么就是在原来大小上加上相应的值,否则就是减去相应的值。
最后调用setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), heightSize) 设置自身的大小。
相信到这里你应该已经对LinearLayout的测量过程有了很深刻的理解了吧,如果还有觉得描述不清楚的地方,欢迎留言讨论...
版权声明:本文为博主原创文章,未经博主允许不得转载。