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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容