本文主要描述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
的onWindowFocusChanged
onWindowFocusChanged
表示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 size
the size of the measure specification
- 应当传入尺寸参数具体数值,而不是LP的
MATCH_PARENT
或WRAP_CONTENT
- 应当传入尺寸参数具体数值,而不是LP的
@param mode
the mode of the measure specification
- 应当出入下面三种SpecMode之一:
MeasureSpec.AT_MOST
MeasureSpec.EXACTLY
MeasureSpec.UNSPECIFIED
- 应当出入下面三种SpecMode之一:
请构造正确的参数,不要使用错误的调用姿势,比如:
int wMSpec = View.MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
系统无法根据这俩参数得到合法的SpecMode,也不一定能measure出正确的结果。