MeasureSpec
基本概念
MeasureSpec参与了View的measure过程,系统根据MeasureSpec来测量出View的测量宽/高
- 对于DecorView,其MeasureSpec是由窗口尺寸的大小和自身的LayoutParams来共同确定的
- 对于普通的View,是由父容器的MeasureSpec和自身的LayoutParams共同决定的
一个MeasureSpec由MeasureMode和MeasureSize组成,分别指测量模式和规格大小。
public static class MeasureSpec {
//偏移量
private static final int MODE_SHIFT = 30;
//1100 0000 0000 0000 0000 0000 0000 0000
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
//合并SpecSize和SpecMode生成MeasureSpec
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
从上面的源码可以看到MeasureSpec是一个32位int值,高两位代表SpecMode,低30位代表SpecSize。SpecMode有三种模式:
- UNSPECIFIED:父容器不对View有任何的限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态。
- EXACTLY:父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize指定的值。他对应于LayoutParams中的match_parent和具体的数值这两种情况。
- AT_MOST:父容器指定了一个可用大小,View的大小不能超过父容器的SpecSize。具体的值要看不同View的具体实现,它对应于LayoutParams中的wrap_content。
DecorView的MeasureSpec创建过程
在ViewRootImpl中的measureHierarchy方法中有如下一段代码:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
其中传入的参数desiredWindowWidth、desiredWindowHeight是屏幕的宽度和高度,接着再看下getRootMeasureSpec方法的实现:
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
根据LayoutParams中的宽/高的参数来划分:
- LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小;
- LayoutParams.WTAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
- 固定大小:精确模式,大小为LayoutParams中指定的大小。
普通View的MeasureSpec创建过程
这里的View指的是我们布局中的View,View的measure过程由ViewGroup传递而来,先看下ViewGroup的measureChildWithMargins方法:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子元素的布局参数
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
从上面的代码看出,子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关。得到子元素MeasureSpec的具体情况可以看下getChildMeasureSpec()方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//获取子view的真实宽度
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 若当前view的SpecMode是EXACTLY
case MeasureSpec.EXACTLY:
//宽/高是固定的数据
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
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) {
//如果设置了sUseZeroUnspecifiedMeasureSpec,大小就是0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//合并得到MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上述代码主要作用是根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec。
child布局参数/父元素Mode | EXACTLY | AT_MOST | USPECIFIED |
---|---|---|---|
dp\px | EXACTLY childSize |
EXACTLY childSize |
EXACTLY childSize |
match_parent | EXACTLY parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize |
AT_MOST parentSize |
UNSPECIFIED 0 |
总结
- Activity对象创建完毕后,会将DecorView添加到Window中,同时创建ViewRootImpl对象与DecorView创建联系
- View的绘制流程从ViewRootImpl的PerformTraversals()方法开始。performTraversals()先计算出窗口的大小,再通过getRootMeasure()方法,计算出DecorView的宽高,传入performMeasure()方法中
private void performTraversals() {
...
if (!mStopped) {
boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
(relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
// 获取测量规格,mWidth 和 mHeight 当前视图 frame 的大小
// lp是WindowManager.LayoutParams
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
...
}
}
- 开始view的measure,起作用的其实是onMeasure()方法
View的measure过程
View的measure方法中回去调用View的onMeasure方法,如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension()会设置View宽高的测量值
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;
}
当AT_MOST和EXACTLY这两种情况时,返回的值都是measureSpec的specSize,specSize是测量后的大小。
UNSPECIFIED这种情况,一般用于系统内部的测量,在这种情况下,View的大小为getDefaultSize的第一个参数size,宽高分别为getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的返回值。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
这里判断了View是否设置了背景,若设置了背景,View的宽度为mMinWidth。mMinWidth对应于android:minWidth这个属性所指的值,若不设置默认为0。如果View设置了背景,则View宽度为max(mMinHeight, mBackground.getMinimumHeight()):
public int getMinimumHeight() {
final int intrinsicHeight = getIntrinsicHeight();
return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
//返回drawable内在高度,如果drawable没有内在高度返回-1
public int getIntrinsicHeight() {
return -1;
}
根据上述代码,得到getMinimumHeight()返回的就是drawable的原始高度,前提是drawable有原始高度,没有就返回0。例如:ShapeDrawable无原始宽/高,BitmapDrawable有原始宽/高。
wrap_content和自定义问题
从getDefaultSize()方法来看,若按照onMeasure的默认方法实现,当宽/高设置成wrap_content或match_parent时,最后的结果都是specSize,也就是两种情况下都是match_parent的效果。所以在自定义view下的时候需要对wrap_content进行处理。
在自定义view的时候,我们通过判定宽高是否是wrap_content来给宽/高是wrap_content的情况设定一个默认的内部宽/高。像TextView、ImageView都对这里做了处理。
View的Measure过程总结
- 从ViewGroup#measureChildWidthMargins开始,计算View的MeasureSpec
- measureChildWidthMargins()调用child.measure()开始测量过程
- View#measure()调用onMeasure()
- onMeasure()方法测量View宽高
- 根据是否有背景图来确定View的默认大小
- 根据SpecMode来确定View测量后的大小是SpecSize还是默认大小
- onMeasure()测量后的数据给全局变量mMeasuredWidth和mMeasuredHeight
ViewGroup的measure过程
对于ViewGroup来说,除了完成自己的measure过程外,还会遍历去调用所有子元素的measure方法,各个子元素再去递归执行这个过程。
ViewGroup#measureChildren
遍历子view调用measureChild()
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];
//当child不是GONE时,就去测量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
ViewGroup#measureChild()
- 前面提到过的measureChildWithMargins()中的getChildMeasureSpec(),开始测量
- 调用子view向下递归测量
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没有定义测量的具体过程,因为ViewGroup是一个抽象类,其测量过程的onMeasure方法需要各个子类去具体实现。
获得View宽高的四种方法
在实际操作中我们可能回去获取某个view的宽和高,但是在onCreate() onResume()方法中一般是获取不到的,因为View的measure过程和Activity生命周期方法不是同步执行的。
1.Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了。当Activity窗口得到焦点和失去焦点是均会被调用一次,当频繁进行onPause和onPause,会被频繁调用。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
2.view.post(runnable)
通过post可以将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
3.ViewTreeObserver
可以通过ViewTreeObserver添加监听来监听View的各种动态。onGlobalLayout()会在View树发生改变或是View树内部的View的可见性发生改变时调用。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
4.手动measure(view.measure(int widthMeasureSpec,int heightMeasureSpec))
对view进行measure得到view的宽 高,需要根据view的LayoutParams来分:
match_parent
无法measure出具体宽高。因为要测量出当前view尺寸,需要知道父容器的属于空间,无法知道parentSize。
wrap_content
((1 << 30) - 1)是specMode支持的最大值
int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
具体数值
LayoutParams lp = view.getLayoutParams();
int widthSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(lp.height, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
网友评论