Android(安卓)View 绘制流程之一:measure测量
Android View 绘制流程之一:measure测量
- 一.MeasureSpec测量规格
- 二.LayoutParams布局参数
- 1.View
- addView
- LinearLayout的generateDefaultLayoutParams
- 2.xml
- 三.Measure整体流程
- 四.常见onMeasure实现
- 1.测量子View方法
- (1)measureChildren()
- (2)measureChild()
- (3)measureChildWithMargins()
- (4)getChildMeasureSpec(int spec, int padding, int childDimension)
- (5)resolveSizeAndState(size,measureSpec,state)
- 2.setMeasuredDimension()
- (1)FrameLayout的onMeasure
- (2)LinearLayout的onMeasure
- (3)RelativeLayout的onMeasure
系列文章:
Android View 绘制流程之一:measure测量
Android View 绘制流程之二:layout布局
Android View 绘制流程之三:draw绘制
Android View 绘制流程之四:绘制流程触发机制
measure方法是View测绘系统的第一步,主要是需要重写onMeasure方法根据自己的规格来测量出真实尺寸,如果是ViewGroup的话,还要在onMeasure中根据规格和布局属性测量子view,调用其measure方法。
一.MeasureSpec测量规格
对于view的尺寸正如我们XML中定义的一样,可以是具体值xxxdp,也可以是相对值MATCH_PARENT、WRAP_CONTENT,如果都是具体值则其实不需要measure过程,直接遍历所有view为其设置具体的宽高即可完成大小的测量,而为了更好的支持"占满全部可用控件"、"包含子view的宽高"这些可以适用屏幕的情况,需要这些相对值,那么就需要一套复杂测量过程,通过父view的规格和view想要的规格计算出view实际的规格,于是View系统中使用MeasureSpec类来将其规格—也就是实际大小和模式(想要的规格)两个int值合成一个31位的int值,高两位代表模式,低30位代表大小。
整个view测量过程中会使用该种形式的值来设置view的规格:
-
MeasureSpec提供了三种模式:
-
EXACTLY:代表规格是一个确定的值
-
AT_MOST:代表规格是一个最大值
-
UNSPECIFIED:代表没有规格限制,view可以决定自己的大小
-
-
一些静态方法
-
makeMeasureSpec(size,mode):将大小和模式生成一个int的规格,高两位代表模式,其余代表尺寸,实际使用的是位运算生成
-
getMode(measureSpec):将一个规格的模式取出,实际使用的是位运算生成
-
getSize(measureSpec):将一个规格的尺寸取出,实际使用的是位运算生成
-
二.LayoutParams布局参数
我们使用view,为其设置各种布局属性,都是通过xml中或者代码动态设置,而在代码里获取属性时经常通过getLayoutParams拿到一个LayoutParam对象,而且还经常要强转(包括后面会看到view系统里也会调用该方法并强转);而我们一般在代码里设置具体LayoutParam子类(否则会因强转异常),或者根本不去手动设置他,但系统获取并强转时却没有错,那么LayoutParam是如何设计以及如何创建的呢?下面我们来看看。
1.View
addView
首先需要确定的是,每个view都需要各种属性(最起码得有宽高),并且最终的展示都需要加入到一个父view里(addView),下面来看下ViewGroup的addView方法
public void addView(View child, int index) { ... LayoutParams params = child.getLayoutParams(); if (params == null) { params = generateDefaultLayoutParams(); ... } addView(child, index, params);}protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);}
可以看到,加入一个view到一个ViewGroup时,需要view的LayoutParams,如果view还没有,会生成一个默认的LayoutParam,而ViewGroup中生成的这个默认的对象就是ViewGroup.LayoutParam对象,而一般的ViewGroup(如LinearLayout)会拿到这个对象强转成MarginLayoutParam(继承自ViewGroup.LayoutParam)对象做有关margin参数的处理,怎么做的呢,其实是基本每个ViewGroup子类都会重写generateDefaultLayoutParams方法,返回其自己的一个继承自MarginLayoutParam的静态内部类,比如LinearLayout的实现:
LinearLayout的generateDefaultLayoutParams
protected LayoutParams generateDefaultLayoutParams() { if (mOrientation == HORIZONTAL) { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } else if (mOrientation == VERTICAL) { return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } return null;}public static class LayoutParams extends ViewGroup.MarginLayoutParams { ... public float weight; ... public int gravity = -1; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout); weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0); gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1); a.recycle(); } public LayoutParams(int width, int height) { super(width, height); weight = 0; }...}
这样一来,比如在LinearLayout内部使用其child的getLayoutParam强转成LinearLayout.LayoutParam就不会有问题了,因此,如果手动为view设置LayoutParam时(setLayoutParam),也需要设置成其相应父类的LayoutParam才行。
2.xml
对于xml中定义的view,LayoutParam如何设置的呢?下面我们来看下
一般我们写完布局文件后,会在activity的setContentView里设置,或是在某些地方使用LayoutInflater的inflate(layoutId,parent,attachToParent)来手动加载一个xml文件为view,而前者也是调用的后者的方法进行加载xml文件,所以我们看一下inflate方法是如何解析xml文件并生产LayoutParam对象的就可以了:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { ... if (TAG_MERGE.equals(name)) {//merge标签的处理 ... } else { // 从xml文件中找到第一个tag作为根view final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { // 根据传入的root来解析根view的LayoutParam,调用的是root的generateLayoutParam(正如之前说过的,一般ViewGroup会重载该方法返回特定的LayoutParam,这个LayoutParam不需要的attrs将失效) params = root.generateLayoutParams(attrs); if (!attachToRoot) {//如果要直接addView则在后续的addView时带入该LayoutParam,不要addView时则给该view手动设置LayoutParam // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } // 继续加载xml中所有的子view,以该根view作为parent传入,用来解析每个子view的LayoutParam,解析完后addView到该根view中,且每个子view的加载会递归进行(子view是ViewGroup的情况)// 所有子view加载完毕后调用view的onFinishInflate方法通知(可以重写该方法,在该方法内使用其子view) rInflateChildren(parser, temp, attrs, true); // 是否直接添加到root里 if (root != null && attachToRoot) { root.addView(temp, params); } // 如果没有加入到root里则返回的是xml中的根view,否则返回的是整个view(传进来的root) if (root == null || !attachToRoot) { result = temp; } }... return result; }}
上面代码注释以及解释的很详细,大概逻辑就是先解析出xml中的根view,根据传入的root设置其LayoutParam,然后以他作为parent去递归解析xml中的子view并将子view addView进去,解析完后,再去判断是否需要将整个view加入到传入的root当中去,并返回即可;注意点如下:
-
所以说如果我们没有传入root,那么xml中的根view的那些attrs都没有用
-
如果我们传入root,比如说一个LinearLayout,那么xml中的根view的LayoutParam会由LinearLayout的generateLayoutParam生成,也就是解析成LinearLayout.LayoutParam;此时如果你手动addView到一个RelativeLayout里就会出现异常了(因为需要的应该时RelativeLayout.LayoutParam)
-
setContentView()时系统会将最外层的一个ViewGroup当作root参数传入到inflate方法中,所以其根view的attrs是有效的;不过这个root是个FrameLayout,也就是说他只生成FrameLayout.LayoutParam,只能解析该LayoutParam的属性,所以,假设你xml的根view是个RelativeLayout,使用了其特有的attr比如android:layout_centerInParent时,就不会被解析进LayoutParam,就会失效,而用android:layout_gravity则可以,因为FrameLayout.LayoutParam里会解析该attr
三.Measure整体流程
上面解释了测量view时使用的规格数据类型,以及view的布局属性的设置,而view的measure过程就会根据要测量的规格和属性决定view的具体大小,下面就说一下measure的整体流程:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { ... if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) {//如果父类施加的规格发生了变化,或者该view有FORCE_LAYOUT标志则说明需要执行measure过程 // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure过程中最重要的一个方法,交由子类重写,来决定自己的真实大小,如果是ViewGroup,要通过测量子view的大小来决定自己的大小 onMeasure(widthMeasureSpec, heightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } else { long value = mMeasureCache.valueAt(cacheIndex); // Casting a long to int drops the high 32 bits, no mask needed setMeasuredDimensionRaw((int) (value >> 32), (int) value); mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; }... mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;//测量完毕,指明view需要布局layout }//保存新的规格 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension}
大致步骤就是先回判断父类的规格较之前是否发生改变,或者view的flag里是否有FORCE_LAYOUT这个标志,如果满足则要进行实际的测量过程,否则不需要测量;测量时调用onMeasure方法交由子类实现自己的测量方式,最终计算出自己的大小(如果是ViewGroup还需递归调用子view的measure方法以便测量子view并根据子view测量结果决定自己的大小),并需要在onMeasure测量出实际大小后调用setMeasuredDimension将宽高保存;接着给view加入LAYOUT_REQUIRED标志使其将被重新布局;最后保存保存新的规格值以便下回比较使用;有几个注意点:
-
measure方法最初是由ViewRootImpl的performMeasure中调用根view的measure方法开始的,那么最初的规格是什么呢?是ViewRootImpl的getRootMeasureSpec生成的,一般就是窗口的宽高值(这部分内容可以参考系列文章)
-
measure方法传入的两个规格到底是什么意思呢?是父类通过他的规格结合子view的规格(width、height属性)来确定的一个规格,最终传给子view时对于子view来说就他自己的尺寸的规格,子view根据这个规格最终计算出自己的大小,具体的确定规则下面会说到
-
view系统的许多标志使用的不是一个一个的变量,而是使用的一个int值,将不同标志位的值通过位运算的方式加到这个变量上,在使用时也是根据位运算判断是否有该标志
-
有FORCE_LAYOUT说明需要重新布局,重新布局则必须要重新测量(这部分内容可以参考系列文章)
四.常见onMeasure实现
onMeasure方法view的measure过程的核心方法,子view应该重写其进行自己尺寸的计算,子ViewGroup也应该重写其进行子view的测量,并且根据子view的测量结果决定自己的尺寸,首先明确几个重要点:
1.测量子View方法
ViewGroup提供了几个重要的基础方法来测量子view
(1)measureChildren()
其实就是遍历children,调用measureChild方法(GONE掉的view不会测量)
(2)measureChild()
该方法里,会调用child的getLayoutParam方法拿到布局参数,然后结合父view传来的规格(也就是measure时传来的两个参数)调用getChildMeasureSpec()方法生成子view的具体宽高规格,并调用child.measure方法进行子view的测量;getChildMeasureSpec方法就是具体规格生成规则的方法,下面会说
(3)measureChildWithMargins()
该方法只是在measureChild方法基础上考虑到了子view的margin属性,所以这里需要将child的LayoutParam强转成MarginLayoutParam使用其margin相关属性,一般情况下的ViewGroup都会生产一个继承自MarginLayoutParam的LayoutParam类,所以在测量时可以调用此方法快捷测量,但是如果自定义的某个ViewGroup没有使用继承MarginLayoutParam的LayoutParam,那么就不能调用此方法,否则会强转时异常;那么getChildMeasureSpec如何使用这些值下面我们来看看
(4)getChildMeasureSpec(int spec, int padding, int childDimension)
这就是根据父类要求的规格和自身的想要规格计算出实际规格的方法,第一个参数是父view要求的规格(宽度或高度的);第二个参数是在该属性(宽度或高度)上已使用的值,计算时应该刨除;第三个是在该属性上子view自己想要的大小(具体值、MATCH_PARENT、WRAP_CONTENT)
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec);//要求的规格的模式 int specSize = MeasureSpec.getSize(spec);//要求的规格的大小 int size = Math.max(0, specSize - padding);//这是实际可以获得的规格大小,因为父view可能有padding,子view可能有margin int resultSize = 0; int resultMode = 0; switch (specMode) {// 父view的规格是一个固定值 case MeasureSpec.EXACTLY: if (childDimension >= 0) {//如果子view的尺寸是一个固定值,那么子view的规格就是固定的(EXACTLY)一个值(childDimension),事实上,只要子view的尺寸固定,那么他的规格也就是某个固定的值,正如上面所说,都是这种情况则不需要有measure过程了 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) {//如果子view尺寸是想要占满父view全部,那么他的规格就是固定的可获得的尺寸(size),因为父view的尺寸是固定的,所以子view的尺寸也就知道了 resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) {//如果子view尺寸时想要包含自身内容即可,那么他的规格就是最多(AT_MOST)是可获得的尺寸(size),这很合乎常理 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 父view的规格是一个"最大值" case MeasureSpec.AT_MOST: if (childDimension >= 0) {//同上 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) {//此时子view想要和父view一样大小,但父view是最大是size,所以理所当然的子view的规格就应该是最大是size resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) {//此时与上面这种情况一样,子view想要包含全部即可,所以他只需要最大不超过父view允许他获得的大小即可 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 这种情况是指父view没有指定规格,要看看子view自己决定自己有多大 case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) {//同上 // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) {//此时和下面这种情况,都会将mode设为UNSPECIFIED,size设为可获得的最大尺寸交由子view去自己处理 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode);//将mode和size用位运算计算出规格}
正如前面所说,measureChild方式向该方法传递的第二个padding参数只是父view的相应padding参数,而measureChildWithMargins方法向其传递的是父view的相应padding参数以及子view的相应margin参数作为已使用的部分,这样大部分ViewGroup就可以快捷的使用该方法而不用考虑margin的事情
(5)resolveSizeAndState(size,measureSpec,state)
该方法是根基测量得到的尺寸size与原有的规格限制,决定最终的尺寸,也就是调整测量得到的尺寸,使其满足原有规格的限制
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { final int specMode = MeasureSpec.getMode(measureSpec); final int specSize = MeasureSpec.getSize(measureSpec); final int result; switch (specMode) { case MeasureSpec.AT_MOST://如果规格限制是"最大值" if (specSize < size) {//那么测量的值如果比最大值还要大,那么最终大小也只能是最大值了,并通过位运算加入TOO_SMALL标志 result = specSize | MEASURED_STATE_TOO_SMALL; } else {//如果不大于最大值,则使用该值为最终值 result = size; } break; case MeasureSpec.EXACTLY://如果规格是固定值,则不论测量出来是多大,就是这个固定值 result = specSize; break; case MeasureSpec.UNSPECIFIED://如果是"没有限制",那么说明任由view决定大小,所以就使用测量出来的实际值即可 default: result = size; } return result | (childMeasuredState & MEASURED_STATE_MASK);}
注释已经很清楚,主要用于测量后,根据测量值调整成满足限制的最终值
2.setMeasuredDimension()
onMeasure测量后,一定要记得调用setMeasuredDimension来保存测量的宽高结果
下面通过几个常见的ViewGroup来说明onMeasure通常是如何测量的
(1)FrameLayout的onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount();//该变量的意思是是否需要测量MATCH_PARENT的子view,成立条件是只要该父View的规格(宽和高)有一个不是确定值,之后就要对这些子view做一些操作,具体操作往下看 final boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; mMatchParentChildren.clear();//清空需要再测量的child集合 int maxHeight = 0; int maxWidth = 0; int childState = 0;//遍历非GONE的child,调用上面说过的measureChildWithMargins方法进行child的测量 for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams();// 每次测量child后,child的尺寸就知道了,要更新当前父view的最大高度和最大宽度 maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, child.getMeasuredState()); if (measureMatchParentChildren) {//这步就是,如果满足上面说的条件,则规格有MATCH_PARENT的child都要加入到这个集合中,下面要做处理 if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } }// 其他一些操作//至此,maxWidth和maxHeight其实就是通过测量子view得出的FrameLayout的宽高了,再调用resolveSizeAndState与父view对其原本的规格要求,得出真正合适的规格尺寸,调用//setMeasuredDimension保存即可 setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT));//这里,就要开始处理上面说的那些child//原因是这样的,有些子view要MATCH_PARENT,但是FrameLayout的规格并不是确定值,所以一开始无法真正实现MATCH_PARENT,只能遍历测量,当测量完毕后,FrameLayout的尺寸知道了,再去//使用新的尺寸去调整这些MATCH_PARENT的child的尺寸,也就是重新生成确定的(EXACTLY)规格让child调用measure,当然这以及不会再影响到FrameLayout的尺寸了 count = mMatchParentChildren.size(); if (count > 1) { for (int i = 0; i < count; i++) { final View child = mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) {//使用getMeasuredWidth(FrameLayout的真实宽度)来生产MeasureSpec final int width = Math.max(0, getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin, lp.width); } final int childHeightMeasureSpec; if (lp.height == LayoutParams.MATCH_PARENT) {//使用getMeasuredHeight(FrameLayout的真实高度)来生产MeasureSpec final int height = Math.max(0, getMeasuredHeight() - getPaddingTopWithForeground() - getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin, lp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec);//重新测量 } }}
这就是FrameLayout的测量过程,大体思路很简单,就是遍历测量child,并找出child的最大宽度和高度即为FrameLayout的宽高(非固定值时),因为FrameLayout就是一个最基本的容器,没有顺序和相对位置的概念;除此之外对MATCH_PARENT的child做了特殊处理,就是在知道FrameLayout的真实尺寸后,再去用这些真实数据去满足child的MATCH_PARENT(重新measure一次即可)
(2)LinearLayout的onMeasure
LinearLayout是线性容器,有水平和垂直方向区分,他的child会按照水平或垂直方向排列,在onMeasure里会根据不同方向调用不同方法测量,这里只说垂直方向调用measureVertical的情况,水平方向类似,不再赘述
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { ... final int count = getVirtualChildCount(); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); boolean matchWidth = false; boolean skippedMeasure = false; ... // 遍历测量child,有weight的child会有一些特殊处理 for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); ... LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); totalWeight += lp.weight;//记录总weight if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {//如果LinearLayout是固定的高度且child没有高度只有weight等待分配剩余控件,则先跳过测量 final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); skippedMeasure = true; } else { int oldHeight = Integer.MIN_VALUE;//这里是个特殊处理:如果LinearLayout高度不固定但是child没有高度,等待分配剩余空间时,就将其高度置为WRAP_CONTENT,否则其最终测量结果就可能是0了 if (lp.height == 0 && lp.weight > 0) { oldHeight = 0; lp.height = LayoutParams.WRAP_CONTENT; } // 该方法内部其实就是调用measureChildWithMargins方法测量child 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));//更新LinearLayout测量的总高度... } ... boolean matchWidthLocally = false; if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {//宽度MATCH_PARENT但是LinearLayout的宽度不知道,要记录一下后面重新测量child // 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);//更新maxWidth 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 { alternativeMaxWidth = Math.max(alternativeMaxWidth, matchWidthLocally ? margin : measuredWidth); } i += getChildrenSkipCount(child, i); } ... int heightSize = mTotalLength; // 检查当前测量的高度与最小高度的比较 heightSize = Math.max(heightSize, getSuggestedMinimumHeight()); // 再与规格限制得出实际的规格(有可能与mTotalLength相等也可能小于mTotalLength) int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0); heightSize = heightSizeAndState & MEASURED_SIZE_MASK; // 这里就要开始重新测量那些有weight的child了 int delta = heightSize - mTotalLength;//LinearLayout剩余可用空间,可能为0可能小于0当然也可能大于0 if (skippedMeasure || delta != 0 && totalWeight > 0.0f) {//如果之前有直接跳过测量的child或是有要分享剩余空间的child时 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) {//该view有weight,要分享 int share = (int) (childExtra * delta / weightSum);//计算出其分享的空间大小,delta可能小于0,也就说明share可能为负的,child的高度可能还要比已有的小 weightSum -= childExtra; delta -= share; final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width); if ((lp.height != 0) || (heightMode != MeasureSpec.EXACTLY)) {//根据上面的判断条件知,这是说明该child在上面测量过 int childHeight = child.getMeasuredHeight() + share;//所以用已有高度加上分享的空间为新的高度 if (childHeight < 0) { childHeight = 0; } child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));//重新测量 } else {//child之前没测过,则直接使用分享的空间作为固定值进行测量 child.measure(childWidthMeasureSpec, MeasureSpec.makeMeasureSpec(share > 0 ? share : 0, MeasureSpec.EXACTLY)); } // Child may now not fit in vertical dimension. childState = combineMeasuredStates(childState, child.getMeasuredState() & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT)); } final int margin = lp.leftMargin + lp.rightMargin; final int measuredWidth = child.getMeasuredWidth() + margin; maxWidth = Math.max(maxWidth, measuredWidth);//更新maxWidth 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 { ... } if (!allFillParent && widthMode != MeasureSpec.EXACTLY) { maxWidth = alternativeMaxWidth; } maxWidth += mPaddingLeft + mPaddingRight; // Check against our minimum width maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), heightSizeAndState);//调整尺寸后设置即可 if (matchWidth) {//需要调整宽度 forceUniformWidth(count, heightMeasureSpec);//用已知的LinearLayout的宽度重新测量那些MATCH_PARENT的child即可 }}
LinearLayout的测量过程大致概括就是先遍历测量一次children(跳过那些只用于weight计算的child),不断更新总测量高度,最大宽度,weight总和等值;然后根据测量得到的高度与规格计算出剩余可用空间,按各自设定的weight分配给那些child使其重新测量即可得到最终的实际总高度;并对那些宽度为MATCH_PARENT的child更新实际的宽度进行重新的测量;注意点:
-
有源码可知,设置weight权重的child,分配的额外空间不一定为正的,还有可能因为总空间越界了反而扣除其一部分空间,也就是好事坏事它都首当其冲
-
有源码可知,当设置了weight时,如果可能,尽量不用设置height的值为WRAP_CONTENT或其他,而设置为0,这样就可能在第一次遍历时不用测量该view,对提升性能更好些
-
有源码可知,对于有设置weight的view的LinearLayout来说,基本都会在LinearLayout的onMeasure中遍历测量其整个view两次,这样如果嵌套里还有有weight的view,就会成指数的遍历测量,需要谨慎这样使用
(3)RelativeLayout的onMeasure
RelativeLayout是一个相对布局的容器,他可以为子view处理相对的关系,比如view1在view2的哪边,或者与哪边对齐,也可以指定与parent的哪边对齐或者整体居中等属性
他的测量过程比较独特:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mDirtyHierarchy) { mDirtyHierarchy = false; sortChildren();//这一步很重要,由于只有children只有相对位置关系,那么该方法会根据相对关系来决定child的处理顺序,先处理被依赖的后才能处理依赖的,RelativeLayout维护两个view数组,分别是处理水平方向位置关系的和垂直方向位置关系关系的,下面使用 } int myWidth = -1; int myHeight = -1; int width = 0; int height = 0; final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); // 记录下来可以确定的值 if (widthMode != MeasureSpec.UNSPECIFIED) { myWidth = widthSize; } if (heightMode != MeasureSpec.UNSPECIFIED) { myHeight = heightSize; } if (widthMode == MeasureSpec.EXACTLY) { width = myWidth; } if (heightMode == MeasureSpec.EXACTLY) { height = myHeight; } View ignore = null; int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; final boolean horizontalGravity = gravity != Gravity.START && gravity != 0; gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK; final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0; int left = Integer.MAX_VALUE; int top = Integer.MAX_VALUE; int right = Integer.MIN_VALUE; int bottom = Integer.MIN_VALUE; boolean offsetHorizontalAxis = false; boolean offsetVerticalAxis = false; if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) { ignore = findViewById(mIgnoreGravity); }//如果不能确定RelativeLayout的宽高则需记录一下,后面要做处理 final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY; final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY; ...//此时开始遍历测量横向关系的view数组,当前的数组以及是按照依赖关系有序的 View[] views = mSortedHorizontalChildren; int count = views.length; for (int i = 0; i < count; i++) { View child = views[i]; if (child.getVisibility() != GONE) {//为GONE不去测量 LayoutParams params = (LayoutParams) child.getLayoutParams(); int[] rules = params.getRules(layoutDirection);//拿到该child的所有规则(依赖关系) applyHorizontalSizeRules(params, myWidth, rules);//将这些规则应用到child上,下面会举一个例子,这里可以先理解为根据其依赖的child(肯定先于该child测量)的ltrb四个属性和其相对位置来计算该child的ltrb measureChildHorizontal(child, params, myWidth, myHeight);//该方法就是使用计算出的lrtb和myWidth按规则得出宽度规格、使用myHeight和params.height按规则得出高度规格,然后调用child.measure测量child//测量完child后,他的具体宽知道了,再去更新他的lr值,就是正确的lr值(没错,其实RelativeLayout此时以及完成layout的大部分工作了,在layout过程会看到) if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) { offsetHorizontalAxis = true; } } }//横行关系测量完了,接下来要测量垂直关系了 views = mSortedVerticalChildren; count = views.length; final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion; for (int i = 0; i < count; i++) { final View child = views[i]; if (child.getVisibility() != GONE) { final LayoutParams params = (LayoutParams) child.getLayoutParams(); applyVerticalSizeRules(params, myHeight, child.getBaseline());//与横行一样,先应用垂直方向的相对位置信息到lrtb上 measureChild(child, params, myWidth, myHeight);//此时宽度规格计算完毕,再根据垂直方向顺序测量一遍即可(宽度的规格和原来逻辑一致即可)//测量完child后,他的具体高知道了,再去更新他的tb值,就是正确的tb值(没错,其实RelativeLayout此时以及完成layout的大部分工作了,在layout过程会看到) if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) { offsetVerticalAxis = true; }//如果RelativeLayout宽度不确定,则要更新最大宽度 if (isWrapContentWidth) { if (isLayoutRtl()) { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { width = Math.max(width, myWidth - params.mLeft); } else { width = Math.max(width, myWidth - params.mLeft - params.leftMargin); } } else { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { width = Math.max(width, params.mRight); } else { width = Math.max(width, params.mRight + params.rightMargin); } } }//如果RelativeLayout高度不确定,则要更新最大高度 if (isWrapContentHeight) { if (targetSdkVersion < Build.VERSION_CODES.KITKAT) { height = Math.max(height, params.mBottom); } else { height = Math.max(height, params.mBottom + params.bottomMargin); } }//根据gravity来记录RelativeLayout的lrtb最值 if (child != ignore || verticalGravity) { left = Math.min(left, params.mLeft - params.leftMargin); top = Math.min(top, params.mTop - params.topMargin); } if (child != ignore || horizontalGravity) { right = Math.max(right, params.mRight + params.rightMargin); bottom = Math.max(bottom, params.mBottom + params.bottomMargin); } } } // 记录baseline,一般使用最开始的一个view作为基准baseline View baselineView = null; LayoutParams baselineParams = null; for (int i = 0; i < count; i++) { final View child = views[i]; if (child.getVisibility() != GONE) { final LayoutParams childParams = (LayoutParams) child.getLayoutParams(); if (baselineView == null || baselineParams == null || compareLayoutPosition(childParams, baselineParams) < 0) { baselineView = child; baselineParams = childParams; } } } mBaselineView = baselineView;//RelativeLayout的宽度未确定 if (isWrapContentWidth) { // Width already has left padding in it since it was calculated by looking at // the right of each child view width += mPaddingRight; if (mLayoutParams != null && mLayoutParams.width >= 0) { width = Math.max(width, mLayoutParams.width); } width = Math.max(width, getSuggestedMinimumWidth()); width = resolveSize(width, widthMeasureSpec);//得到最终的真实宽度//如果需要有"居中"属性的位移,则需要对相应child的lr做相应的移动 if (offsetHorizontalAxis) { for (int i = 0; i < count; i++) { final View child = views[i]; if (child.getVisibility() != GONE) { final LayoutParams params = (LayoutParams) child.getLayoutParams(); final int[] rules = params.getRules(layoutDirection); if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) { centerHorizontal(child, params, width); } else if (rules[ALIGN_PARENT_RIGHT] != 0) { final int childWidth = child.getMeasuredWidth(); params.mLeft = width - mPaddingRight - childWidth; params.mRight = params.mLeft + childWidth; } } } } }//RelativeLayout的高度未确定 if (isWrapContentHeight) { // Height already has top padding in it since it was calculated by looking at // the bottom of each child view height += mPaddingBottom; if (mLayoutParams != null && mLayoutParams.height >= 0) { height = Math.max(height, mLayoutParams.height); } height = Math.max(height, getSuggestedMinimumHeight()); height = resolveSize(height, heightMeasureSpec);//得到真实的最终高度//如果需要有"居中"属性的位移,则需要对相应child的tb做相应的移动 if (offsetVerticalAxis) { for (int i = 0; i < count; i++) { final View child = views[i]; if (child.getVisibility() != GONE) { final LayoutParams params = (LayoutParams) child.getLayoutParams(); final int[] rules = params.getRules(layoutDirection); if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) { centerVertical(child, params, height); } else if (rules[ALIGN_PARENT_BOTTOM] != 0) { final int childHeight = child.getMeasuredHeight(); params.mTop = height - mPaddingBottom - childHeight; params.mBottom = params.mTop + childHeight; } } } } }//还要根据RelativeLayout的gravity属性对child的lrtb做相应的位移 if (horizontalGravity || verticalGravity) { final Rect selfBounds = mSelfBounds; selfBounds.set(mPaddingLeft, mPaddingTop, width - mPaddingRight, height - mPaddingBottom); final Rect contentBounds = mContentBounds; Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds, layoutDirection); final int horizontalOffset = contentBounds.left - left; final int verticalOffset = contentBounds.top - top; if (horizontalOffset != 0 || verticalOffset != 0) { for (int i = 0; i < count; i++) { final View child = views[i]; if (child.getVisibility() != GONE && child != ignore) { final LayoutParams params = (LayoutParams) child.getLayoutParams(); if (horizontalGravity) { params.mLeft += horizontalOffset; params.mRight += horizontalOffset; } if (verticalGravity) { params.mTop += verticalOffset; params.mBottom += verticalOffset; } } } } } ... setMeasuredDimension(width, height);//设置结果}
大致过程就是,按照横向纵向的依赖关系分别排序,保证被依赖的先测量,然后测量依赖的;接着横向测量一遍再纵向测量一遍(测量前先依据其依赖的已经测量完的child的lrtb属性更新自己的lrtb),并更新child的lrtb值;记录RelativeLayout的宽高,并根据gravity和child的相应对齐属性再次移动lrtb值,最终确定child的位置(measure时已经作完,layout时直接使用);最后保存参数即可;
-
测量前,要先应用其指定方向上的依赖属性,这里举一个应用横向依赖的例子,调用applyHorizontalSizeRules方法:
private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules) { RelativeLayout.LayoutParams anchorParams; childParams.mLeft = VALUE_NOT_SET;//没有任何依赖关系值 childParams.mRight = VALUE_NOT_SET; anchorParams = getRelatedViewParams(rules, LEFT_OF);//拿到相对在左关系上的view的params if (anchorParams != null) {//如果有,那么child的right就要设置为被依赖的view的left减去margin的值,由于被依赖的先被测量,所以这时被依赖的view的left肯定也是正确的 childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin + childParams.rightMargin); } else if (childParams.alignWithParent && rules[LEFT_OF] != 0) { if (myWidth >= 0) { childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin; } } //RIGHT_OF... anchorParams = getRelatedViewParams(rules, ALIGN_LEFT);//与其依赖view的左边对齐 if (anchorParams != null) {//所以child的left就是被依赖view的left加上margin即可 childParams.mLeft = anchorParams.mLeft + childParams.leftMargin; } else if (childParams.alignWithParent && rules[ALIGN_LEFT] != 0) { childParams.mLeft = mPaddingLeft + childParams.leftMargin; } //ALIGN_RIGHT... if (0 != rules[ALIGN_PARENT_LEFT]) {//与RelativeLayout左边对其,那么child的left就是RelativeLayout的最左边加上padding和margin的值即可 childParams.mLeft = mPaddingLeft + childParams.leftMargin; } //ALIGN_PARENT_RIGHT...}
其实就是根据布局属性以及被依赖的view以及计算出来的位置属性来计算该child的位置属性进行后续测量
-
RelativeLayout在测量过程就已经将child的lrtb算出,在layout时直接使用即可
-
由源码可知,RelativeLayout在测量时要测量两边children,这一点在考虑性能时要注意,要是嵌套很深的话遍历次数就比较多了
以上是几个常用的ViewGroup的测量过程,当测量到叶子节点也就是具体的某个View时,其onMeasure方法就是测量其自己的实际宽高了,下面拿TextView的测量过程来说明一下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height;//测量文本内容尺寸的对象 BoringLayout.Metrics boring = UNKNOWN_BORING; BoringLayout.Metrics hintBoring = UNKNOWN_BORING; if (mTextDir == null) { mTextDir = getTextDirectionHeuristic(); } int des = -1; boolean fromexisting = false; if (widthMode == MeasureSpec.EXACTLY) {//固定大小直接设置 width = widthSize; } else { if (mLayout != null && mEllipsize == null) { des = desired(mLayout); }//生成Metrics对象 if (des < 0) { boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring); if (boring != null) { mBoring = boring; } } else { fromexisting = true; }//获取内容宽度 if (boring == null || boring == UNKNOWN_BORING) { if (des < 0) { des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint)); } width = des; } else { width = boring.width; } //根据其他属性修改width... width += getCompoundPaddingLeft() + getCompoundPaddingRight();//加上paddings //根据其他属性修改width... // 与最小值比较取最大值 width = Math.max(width, getSuggestedMinimumWidth()); if (widthMode == MeasureSpec.AT_MOST) {//如果是"最大值",则宽度取计算出来的width与规格的宽度的最小值 width = Math.min(widthSize, width); } } //... if (heightMode == MeasureSpec.EXACTLY) {//固定宽度 height = heightSize; mDesiredHeightAtMeasure = -1; } else { int desired = getDesiredHeight();//计算实际想要的高度 height = desired; mDesiredHeightAtMeasure = desired; if (heightMode == MeasureSpec.AT_MOST) {//如果是"最大值",高度为计算出来的与规格的最小值 height = Math.min(desired, heightSize); } } //... setMeasuredDimension(width, height);//设置宽高}
由代码可知,父类已告知TextView的规格,TextView只需根据规格以及自身的属性计算出来的内容宽高,得出实际的宽高并设置即可,也无需测量其子View。
更多相关文章
- android中ContactsContract获取联系人的方法
- [置顶] [Android基础]Android中使用HttpURLConnection
- android之Handler的使用,回到主线程更新UI的四种方法
- Android知识点总结(十五) Android(安卓)MVP 简易模型
- Android(安卓)自定义ContentProvider和ContentObserver的完整使
- 【Android(安卓)应用开发】 自定义组件 宽高适配方法, 手势监听
- Menu
- Android在任意位置由Notification跳向指定fragment
- 面试题总结(2018.7.26开始,持续更新中)