本文主要描述measure过程。
一. View的measure过程
View的measure方法为final,该方法中会调用View的
onMeasure方法。下面是onMeasure方法:protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }setMeasuredDimension会设置宽和高,而对于两个参数的值,我们继续追踪:public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }EXACTLY&&AT_MOST: 直接返回了measureSpec的specSize. 也就是View测量过后的MeasureSpec。UNSPECIFIED: 返回了第一个参数,也就是getSuggestedMinimumWidth()或getSuggestedMinimumHeight()的返回值。一般用于系统内部的测量过程。看看这两个方法的代码:
protected int getSuggestedMinimumHeight() { return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight()); } protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }上面两个方法的源码很简单,首先判断是否设置了背景
mBackground.- 如果没有背景(null),那么返回
mMinHeight和mMinWidth。这两个值对应于android:minHeight和android:minHeight,如果不指定,那么默认值就为0. 如果指定了背景,就返回
max(mMinWidth, mBackground.getMinimumWidth())(以width属性为例)。再次追踪背景Drawable的getMinimumWidth,可以看出getMinimumWidth返回的就是Drawable的原始宽度:public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; }如果没有有原始宽度,就会返回0。一般来说
ShapeDrawable没有原始宽高,BitmapDrawable有原始宽高,即图片的尺寸。
- 如果没有背景(null),那么返回
根据
getDefaultSize方法的实现来看,如果自定义控件直接继承View,就需要重写onMeasure方法,否则即使使用wrap_content,其效果等同于match_parent。自定义View使用
wrap_content,那么View.specMode是AT_MOST, View的宽高等于specSize。在这种情况下,View的specSize是parentSize, 而parentSize是父容器中目前可以使用的大小。此时View的宽高就是父容器当前剩余的空间大小——与match_parent效果一致。解决方案:给View指定一个默认的内部宽高(
width和height):@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int wSpecMode = MeasureSpec.getMode(widthMeasureSpec); int wSpecSize = MeasureSpec.getSize(widthMeasureSpec); int hSpecMode = MeasureSpec.getMode(heightMeasureSpec); int hSpecSize = MeasureSpec.getSize(heightMeasureSpec); if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(width,height); } else if (wSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(width, hSpecSize); } else if (hSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(wSpecSize,height); } }内部宽高的默认值,要根据自定义View的实际情况来定。
二. ViewGroup的measure过程
ViewGroup除了完成自己的measure过程,还会遍历去调用所有子元素的measure方法,各个子元素再递归地执行这个过程。ViewGroup是个抽象类,它没有重写View的onMeasure方法,而是提供了measureChildren方法:protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }在循环中,对每个可见的子元素调用
measureChild方法://取出子元素的LP参数,通过getChildMeasureSpec来创建子元素的MeasureSpec //最后将这个MeasureSpec传递给子元素的measure方法来进行测量 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }ViewGroup没有实现onMeasure,所以也没有测量的具体实现。因为不同的ViewGroup子类,有不同的测量特性,它们需要不同的测量实现。LinearLayout的measure:protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }我们选择查看Vertical情况下的
measureVertical……的一小段://在measureVertical方法中 for (int i = 0; i < count; ++i) { ... final View child = getVirtualChildAt(i); final int usedHeight = totalWeight == 0 ? mTotalLength : 0; measureChildBeforeLayout(child, i, widthMeasureSpec, 0, heightMeasureSpec, usedHeight); final int childHeight = child.getMeasuredHeight(); final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); ... }遍历子元素,并对每个子元素调用
measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法。void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); }同时通过
mTotalLength来存储LinearLayout竖直方向上的初步高度。每测量一个子元素,mTotalLength就会增加一次: 子元素的高度+子元素的topMargin和bottomMargin+下一个子元素的位置偏移量。当所有子元素测量完毕,
LinearLayout就会根据子元素的情况来测量自己的大小。- 针对竖直布局而言,它在水平方向的测量过程遵循View的测量过程。但竖直方向则不同:
- 如果该布局的高度是
match_parent或数值,那么它的测量过程和View一致,即高度为specSize. - 如果该布局的高度是
wrap_content,那么它的高度是所有子元素占用的高度之和,但不能超过其父容器的剩余空间。 - 其最终高度还要考虑竖直方向的padding。
- 如果该布局的高度是
三. 获取View的宽和高
- 问题:在
Activity的onCreate、onResume、onStart中,获取到的View宽高都是0。怎样才能获取到正确的宽和高呢?
- 无法正确获取的原因很简单,前文也说过,在我们获取宽高的时候,View的measure尚未完成。
解决办法1:覆盖
Activity/view的onWindowFocusChangedonWindowFocusChanged表示View已经初始化完毕了,此时来获取宽高是没有问题的。- 正如方法名,每次Activity的窗口得到或失去焦点时,均会被调用一次。所以,当Acitivity继续执行和暂停执行时,
onWindowFocusChanged均会被调用,比如当onResume和onPause被频繁调用时。 使用示例:
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { int width = btnAddHttp.getMeasuredWidth(); int height = btnAddHttp.getMeasuredHeight(); } }
解决办法2:
view.post(runnable)- 通过post将一个
raunnable传递给消息队列,然后等待Looper调用此runnable的时候,View也已经初始化完毕了。 使用示例:
@Override protected void onStart() { super.onStart(); btnAddHttp.post(new Runnable() { @Override public void run() { int width = btnAddHttp.getMeasuredWidth(); int height = btnAddHttp.getMeasuredHeight(); } }); }
- 通过post将一个
解决办法3:
ViewTreeObserver- 正如其名,这是一个View树观察者,我们为这个观察者设置回调接口
OnGlobalLayoutListener,然后当View的状态发生改变时,观察者就会被通知到。 - 随着View的状态改变,观察者会被多次通知,而回调接口也会随之被调用多次。
使用示例:
@Override protected void onStart() { super.onStart(); final ViewTreeObserver viewTreeObserver = btnAddHttp.getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { viewTreeObserver.removeGlobalOnLayoutListener(this); int width = btnAddHttp.getMeasuredWidth(); int height = btnAddHttp.getMeasuredHeight(); } }); }
- 正如其名,这是一个View树观察者,我们为这个观察者设置回调接口
解决办法4:
view.measure(int widthMesasureSpec, int heightMeasureSpec)通过手动对View进行measure来得到View的宽高。这里要根据其LP参数来区分具体的情况:
match_parent:在这种情况下,无法measure出具体的宽和高。因为按照measure过程和MeasureSpec机制,此时我们无法获得父容器的剩余大小,所以理论上无法测量出View的具体大小。具体数值(dp/px):如果宽和高分别设置为100和50像素,可以使用下面的measure方法:
int wMSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY); int hMSpec = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.EXACTLY); btnAddHttp.measure(wMSpec,hMSpec);wrap_content:int wMSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST); int hMSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST); btnAddHttp.measure(wMSpec,hMSpec);注意这个maic number:
(1 << 30) - 1,观察MeasureSpec源码会发现View的尺寸使用30位2进制表示,所以其最大的值是2^30 - 1,也就是我们构造的这个magic number。在最大化模式下,使用理论上的最大值去构造MeasureSpec是合理的。
- 不太推荐此法,有很多局限性,比如
match_parent的情况。
对
View.MeasureSpec.makeMeasureSpec方法的参数说明@param sizethe size of the measure specification
- 应当传入尺寸参数具体数值,而不是LP的
MATCH_PARENT或WRAP_CONTENT
- 应当传入尺寸参数具体数值,而不是LP的
@param modethe mode of the measure specification
- 应当出入下面三种SpecMode之一:
MeasureSpec.AT_MOSTMeasureSpec.EXACTLYMeasureSpec.UNSPECIFIED
- 应当出入下面三种SpecMode之一:
请构造正确的参数,不要使用错误的调用姿势,比如:
int wMSpec = View.MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);系统无法根据这俩参数得到合法的SpecMode,也不一定能measure出正确的结果。
View工作原理系列博客
View的工作原理(一):MeasureSpec
View的工作原理(二):measure
View的工作原理(三):layout与draw
View的工作原理(四):自定義View
本文深入解析Android中View的测量过程,包括View与ViewGroup的measure方法实现细节,以及如何正确获取View的宽高。介绍了四种获取View宽高的方法,并探讨了自定义View时的注意事项。
1269

被折叠的 条评论
为什么被折叠?



