一、MeasureSpec
MeasureSpec是View的一个静态内部类。他是测量过程的依据,所以先要了解这个类。它是一个32位int型的量,高2位表示SpecMode,低30位表示SpecSize。
1. SpecMode分类
- UNSPECIFIED(00):父容器不对View做任何限制。
- EXACTLY(01):精确大小,测量大小为SpecSize。对应具体数值或match_parent。
- AT_MOST(10):父容器指定最大值。对应wrap_content。
2. MeasureSpec类
public static class MeasureSpec {
// mode需要的偏移量
private static final int MODE_SHIFT = 30;
// 11 000000000000000000000000000000
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public @interface MeasureSpecMode {}
// 三种SpecMode对应的值
// 00 00...0
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// 01 00...0
public static final int EXACTLY = 1 << MODE_SHIFT;
// 10 00...0
public static final int AT_MOST = 2 << MODE_SHIFT;
// 合并SpecMode和SpecSize生成MeasureSpec
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
// 加判断UNSPECIFIED
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
// 从MeasureSpec中提取SpecMode
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
// 从MeasureSpec中提取SpecMode
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
// 调整MeasureSpec中的SpecSize
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
// UNSPECIFIED不需要调整
if (mode == UNSPECIFIED) {
return makeMeasureSpec(size, UNSPECIFIED);
}
// 调整SpecSize
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
// 重新打包
return makeMeasureSpec(size, mode);
}
public static String toString(int measureSpec) {
// ...
}
}
3. MeasureSpec的创建依据
- 对于DecorView的确定因素
- 窗口尺寸
- 子身的LayoutParams
- 对于普通View的确定因素
- 父布局的MeasureSpec
- 自身的LayoutParams
一个View的MeasureSpec确定后,View的宽高会在onMeasure()根据MeasureSpec来确定。
4. DecorView的MeasureSpec创建
View的绘制流程是从ViewRootImpl的performTraversals()开始的,其中会调用measureHierarchy()创建DecorView的MeasureSpec。
ViewRootImpl # measureHierarchy()
// ......
// desiredWindowWidth为窗口的宽度,lp.width为DecorView的layourParams中设置的宽度
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
// 得到DecorView宽高的MeasureSpec后调用performMeasure()去完成DecorView的测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
// ......
ViewRootImpl # getRootMeasureSpec()
根据window尺寸和DecorView的LayourParams来决定DecorView的MeasureSpec。
// windowSize就是Window的大小,rootDimension指的是DecorView的layoutParams中设置的大小
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
// match_parent和fill_parent的值都是-1
case ViewGroup.LayoutParams.MATCH_PARENT: windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
// DecorView设置的具体数值
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
ViewRootImpl # performMeasure()
传入创建好的MeasureSpec,调用View的measure()开始测量过程。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
// 开始测量过程
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
总结
DecorView的MeasureSpec由自身LayoutParams和Window尺寸决定。
layoutParams\MeasureSpec | SpecSize | SpecMode |
---|---|---|
MATCH_PARENT | windowSize | EXACTLY |
WRAP_CONTENT | windowSize | AT_MOST |
具体数值 | rootDimension | EXACTLY |
5. 普通View的MeasureSpec创建
普通View的measure()是由ViewGroup的measureChildWithMargins()调用的。
ViewGroup # measureChildWithMargins()
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 参数中的两个MeasureSpec是ViewGroup的MeasureSpec
// widthUsed和heightUsed已经占用的宽度和高度
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 传入ViewGroup的MeasureSpec、ViewGroup中不属于自己的大小、View的layoutParams中定义的大小。
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);
// 计算出MeasureSpec后开始进行测量流程。
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
ViewGroup # getChildMeasureSpec()
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 父布局的Mode和Size
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 父布局原大小减去已用大小如果小于0,就取0
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 如果父布局Mode是EXACTLY
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// 自身设置的是具体数值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 自身设置的是match_parent
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 自身设置的是wrap_content
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父布局Mode为AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 自身设置的是具体数值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 自身设置的是match_parent
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 自身设置的是wrap_content
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 自身设置的是具体数值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 自身设置的是match_parent
// 如果设置了sUseZeroUnspecifiedMeasureSpec,大小就是0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 自身设置的是wrap_content
// 如果设置了sUseZeroUnspecifiedMeasureSpec,大小就是0
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
// 合并返回
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
总结
普通View的MeasureSpec由父布局的MeasureSpec和自身的LayoutParams决定。
自身lp\父布局mode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
具体数值 | childSize EXACTLY | childSize EXACTLY | childSize EXACTLY |
match_parent | parentSize EXACTLY | parentSize AT_MOST | 0 or parentSize UNSPECIFIED |
wrap_content | parentSize AT_MOST | parentSize AT_MOST | 0 or parentSize UNSPECIFIED |
二、View的measure过程
在measure()之前,会确定View的MeasureSpec,在前面已经说了。
1. View # measure()
- ViewGroup的measureChild()调用measure()
- 是测量过程的入口
- 是一个fianl的方法
- 内部会调用onMeasure()
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ......
// 内部会调用onMeasure(),自定义View可以去重写这个方法来满足我们的自定义测量需求。
onMeasure(widthMeasureSpec, heightMeasureSpec);
// ......
}
2. View # onMeasure()
- 在自定义View时通常会重写该方法以满足特定的需求
- 首先会获取期望默认最小值
- 根据默认值和specSize来确定测量宽高
- 保存计算完成的测量宽高
// 根据MeasureSpec测量View的宽高并保存到mMeasureWidth/Height
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
3. View # getSuggestedMinimumWidth/Height()
获得默认宽高。
// 这里获取到了建议最小宽度,判断了该View是否含有背景图
// 如果没有,就是全局mMinWidth
// 如果设置了,就是mMinWidth和背景图片的原始宽度的大者
// 并不是所有图片都有原始宽度。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
// Drawable # getMinimumWidth()
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
// Drawable # getIntrinsicWidth()
// Drawable的 getIntrinsicHeight()默认返回-1,可以重写。
// 比如BitmapDrawable重写了,ShapeDrawable没有。
public int getIntrinsicWidth() {
return -1;
}
4. View # getDefalutSize()
// 根据最小值和MeasureSpec来确定测量宽高
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
// 如果是UNSPECIFIED,测量大小为建议大小。
// AT_MOST或EXACTLY,测量大小为MeasureSpec中计算好的大小。
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
5. wrap_content和自定义的问题
会发现如果按照View的onMeasure()的默认实现,设置match_parent和wrap_content的效果是一样的。因为两种模式下的specSize都是parentSize(见前面分析View的MeasureSpec的生成),而getDefaultSize()中的最终测量宽高取得就是specSize,所以两种情况下都是match_parent的效果。所以在自定义View的时候如果需要对wrap_content做处理。
对于TextView、Button、ImageView等,它们的onMeasure方法系统了都做了重写,不会这么简单直接拿 specSize来当大小,而去会先去测量字符或者图片的高度等,然后拿到View本身content这个高度(字符高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度(字符高度等),而不是像View.java 直接用MeasureSpec的size做为View的大小。
6. View的Measure过程总结
- 从measureChildWithMargins()开始,计算View的MeasureSpec。
- measureChildWithMargins()调用measure,开始测量过程。
- measure()调用onMeasure()。
- onMeasure()中确定测量宽高。
- 根据背景图的最小尺寸和全局mMinWidth/Height确定默认值。
- 根据specMode来决定最终尺寸是取默认值还是specSize。
- onMeasure()确定测量尺寸后设置给全局变量。
三、ViewGroup的measure过程
因为不同ViewGroup的布局相差太多,无法做统一实现,所以不像View一样,ViewGroup没有实现onMeasure,但是提供了measureChildren()向下分发测量过程。
1. ViewGroup # measureChildren()
会遍历子View调用measureChild()。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
// 遍历子View
for (int i = 0; i < size; ++i) {
final View child = children[i];
// 如果这个子View不为GONE,就调用measureChild()去测量
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
2. ViewGroup # measureChild()
- 调用到了前面提到的计算MeasureSpec的getChildMeaureSpec()。
- 调用子View的measure向下递归测量。
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
// 计算MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 开始子View的测量流程
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
三、测量宽高的获取
1. 注意点
- onMeasure()中获取的测量宽高可能不准确,因为某些情况下,会多次measure才能确定最终的测量宽高。
- 在Actvity的onCreate()、onStart()、onResume()中获取有可能会获取不到,因为View的测量和Activity的生命周期不是同步的。在没有测量完毕时获取到的宽高为0。
2. 获取方式
测量结束后,可以通过getMeasuredWidth/Height()获取。这里的重点是确定合适测量结束。
3. 测量结束的时机
Activity/View # onWindowFocusChanged
这个方法会在View初始化完毕后Activity得到焦点或失去焦点时被调用。当频繁进行onResume()和onPause()时,这个方法就会频繁地被调用。
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
view.post(runnable)
performTraversals也是通过向消息队列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();
}
});
}
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();
}
});
}
手动measure
可以手动对View进行measure来获得View的测量宽高,需要根据View的LayoutParams来分。
- match_parent
这种情况无法使用手动mesaure来实现,因为根据MeasureSpec的创建,当自身lp为match_parent时,specSize为parentSize,这种方法无法获取parentSize,所以不可行。
- 具体数值
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);
- 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);
网友评论