调用view.measure(0,0)时发生了什么
在 Activity 的 onCreate、onStart、OnResume 生命周期中,无法直接得到 View 的宽高信息。
网上有以下几种常见的解决办法:
- 在 Activity#onWindowFocusChanged 回调中获取宽高。
- view.post(runnable),在 runnable 中获取宽高。
- ViewTreeObserver 添加 OnGlobalLayoutListener,在 onGlobalLayout 回调中获取宽高。
- 调用 view.measure(),再通过 getMeasuredWidth 和 getMeasuredHeight 获取宽高。
其中第四种方法,网上有很多直接传递两个0的写法,即 view.measure(0,0).
接下来会分析传递的两个0在程序内部发生了些什么,为什么调用之后就能获取 View 的宽高?
- 了解 MeasureSpec
measure(int widthMeasureSpec,int heightMeasureSpec) 的参数是两个符合 MeasureSpec 规范的 int 值。
MeasureSpec 代表一个32位的 int 值,高2位代表 SpecMode,低30位代表 SpecSize. - SpecMode
测量模式,有以下三类。
UNSPECIFIED
EXACTLY
AT_MOST
-
SpecSize
对应测量模式下规格的大小。 -
生成 MeasureSpec
一组 SpecMode 和 SpecSize 可以打包成一个 MeasureSpec:
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
- 获取 SpecMode 和 SpecSize
一个 MeasureSpec 同样可以解包为一组 SpecMode 和 SpecSize:
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
- 0所对应的 MeasureSpec
现在知道了传递的0并不是简单的一个0,它符合着 MeasureSpec 规范。
将0解包后,所对应的 SpecMode = 0,SpecSize = 0.
SpecMode 0 对应的模式为 UNSPECIFIED.
UNSPECIFIED的官方解释:
The parent has not imposed any constraint on the child. It can be whatever size it wants.
父容器不会对子元素加以任何约束,子元素可以是任何大小。
- 创建一个简单的项目
//MainActivity.java 部分代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView imgView = (ImageView) findViewById(R.id.imgView);
imgView.measure(0, 0);
Log.i(TAG, "imageView MeasuredWidth = " + imgView.getMeasuredWidth());
Log.i(TAG, "imageView MeasuredHeight = " + imgView.getMeasuredHeight());
}
<?xml version="1.0" encoding="utf-8"?>
<!-- activity_main.xml -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.gujin.measure_demo.LogImageView
android:id="@+id/imgView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@mipmap/ic_launcher"/>
</LinearLayout>
//LogImageView.java 部分代码
public class LogImageView extends ImageView {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMeasureSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMeasureMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMeasureSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMeasureMode = MeasureSpec.getMode(heightMeasureSpec);
Log.i(TAG, "widthMeasureSize = " + widthMeasureSize);
Log.i(TAG, "widthMeasureMode = " + widthMeasureMode);
Log.i(TAG, "heightMeasureSize = " + heightMeasureSize);
Log.i(TAG, "heightMeasureMode = " + heightMeasureMode);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
-
代码分析
有了项目以后,在 LogImageView#onMeasure()#super.onMeasure() 处打上断点。
观察调用栈:
调用栈
从调用栈中可以看到当调用 imgView.measure(0, 0) 时,执行了继承自父类 View 的 measure 方法,measure 方法中调用了 LogImageView 重写过的 onMeasure 方法,打印log如下:
I/LogImageView: widthMeasureSize = 0
I/LogImageView: widthMeasureMode = 0
I/LogImageView: heightMeasureSize = 0
I/LogImageView: heightMeasureMode = 0
接着会执行 super.onMeasure,在 ImageView 的 onMeasure 中进行实际的测量。
ImageView 的 onMeasure 方法比较长,进行了删减,只描述一下大概逻辑:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int w; //宽
int h; //高
w = mDrawableWidth;
h = mDrawableHeight;
int widthSize;
int heightSize;
w = Math.max(w, getSuggestedMinimumWidth());
h = Math.max(h, getSuggestedMinimumHeight());
widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
setMeasuredDimension(widthSize, heightSize);
}
首先让宽高等于 ImageView 中 Drawable 的宽高,接着调用 getSuggestedMinimumWidth/Height 方法取较大值重新赋给宽高。
- 分析 getSuggestedMinimumWidth:
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
如果 view 没有 background 则返回最小宽度,否则比较 view 的最小宽度和 background 的最小宽度返回较大值。
view 的最小宽度 MinWidth 就是在 xml 中定义 android:minWidth 的值,或者是通过调用 view.setMinimumWidth 设置的最小宽度。
getSuggestedMinimumHeight 方法同理。
然后通过 resolveSizeAndState 方法计算 widthSize 和 heightSize.
- 分析 resolveSizeAndState:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = View.MeasureSpec.getMode(measureSpec);
final int specSize = View.MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case View.MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case View.MeasureSpec.EXACTLY:
result = specSize;
break;
case View.MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
上面分析过,measure(0,0) 传递的0解包后对应的 SpecMode 为 UNSPECIFIED。
可以看到 specMode 为 UNSPECIFIED 时返回值 result 直接等于了 size,而在 EXACTLY 和 AT_MOST 情况中受到了 SpecSize 的影响,这也解释了官方定义中说 UNSPECIFIED 模式下父容器不会对子元素加以任何约束的原因。
函数结尾 result | (childMeasuredState & MEASURED_STATE_MASK),childMeasuredState 传递进来为0,和 MEASURED_STATE_MASK 与运算后结果为0,result 和0进行或运算保持不变。
所以最后的 return 值就是传递进来的 size。
最终调用父类 View 的 setMeasuredDimension 方法将计算出的 widthSize 和 heightSize 传递到 View 中。
至此 ImageView 的 onMeasure 方法分析完毕。
接下来在 View 内继续分析:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
可以看到在 setMeasuredDimension 方法中参数最终传递给 setMeasuredDimensionRaw 方法。
在这里,经过一系列计算的 measuredWidth 和 measuredHeight 赋给了成员变量 mMeasuredWidth 和 mMeasuredHeight,然后将 mPrivateFlags 状态位设置为 PFLAG_MEASURED_DIMENSION_SET.
至此,调用 view.measure(0,0) 之后的计算得出的宽高值已经保存到成员变量中。
- 取宽高
现在调用 getMeasuredWidth/Height 方法就已经可以获得测量后的宽高。
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
MEASURED_SIZE_MASK 的值为 0x00ffffff,和 mMeasuredWidth 进行与运算后,可以将 mMeasuredWidth 的高8位全置0,去掉其他信息。
但是上边说 MeasureSpec 中高2位为 SpecMode,其余30位为 SpecSize,为什么将高8位置0?
还记得 resolveSizeAndState 方法么:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
...
switch (specMode) {
case View.MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
}
...
break;
...
}
}
specSize 和 MEASURED_STATE_TOO_SMALL 进行了或运算,MEASURED_STATE_TOO_SMALL 的值为 0x01000000.
也就是说 SpecSize 的高6位(MeasureSpec 的高3~8位)会记录 STATE 信息,所以要将高8位全置0。
最终在 Log 中可以看到取出的宽高值:
I/MainActivity: imageView MeasuredWidth = 144
I/MainActivity: imageView MeasuredHeight = 144
最后
写本文的初衷是很久以前我就在使用 view.measure(0,0) 来获取宽高,但一直不知道为什么,0是什么意思?传递1进去行不行?终于决定自己分析一下这个困扰已久的问题。
断断续续加起来大概6个小时把这篇文章写完,希望对大家也有所帮助。
如果有分析不对的地方或是其他建议,欢迎留言探讨,谢谢。