调用view.measure(0,0)时发生了什么

在 Activity 的 onCreate、onStart、OnResume 生命周期中,无法直接得到 View 的宽高信息。
网上有以下几种常见的解决办法:

  1. 在 Activity#onWindowFocusChanged 回调中获取宽高。
  2. view.post(runnable),在 runnable 中获取宽高。
  3. ViewTreeObserver 添加 OnGlobalLayoutListener,在 onGlobalLayout 回调中获取宽高。
  4. 调用 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());
    }




    

//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() 处打上断点。
    观察调用栈:


    调用view.measure(0,0)时发生了什么_第1张图片
    调用栈

    从调用栈中可以看到当调用 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个小时把这篇文章写完,希望对大家也有所帮助。
如果有分析不对的地方或是其他建议,欢迎留言探讨,谢谢。

你可能感兴趣的:(调用view.measure(0,0)时发生了什么)