調(diào)用view.measure(0,0)時(shí)發(fā)生了什么

在 Activity 的 onCreate季惯、onStart吠各、OnResume 生命周期中,無法直接得到 View 的寬高信息勉抓。
網(wǎng)上有以下幾種常見的解決辦法:

  1. 在 Activity#onWindowFocusChanged 回調(diào)中獲取寬高走孽。
  2. view.post(runnable),在 runnable 中獲取寬高琳状。
  3. ViewTreeObserver 添加 OnGlobalLayoutListener,在 onGlobalLayout 回調(diào)中獲取寬高盒齿。
  4. 調(diào)用 view.measure()念逞,再通過 getMeasuredWidth 和 getMeasuredHeight 獲取寬高。

其中第四種方法边翁,網(wǎng)上有很多直接傳遞兩個(gè)0的寫法翎承,即 view.measure(0,0).
接下來會(huì)分析傳遞的兩個(gè)0在程序內(nèi)部發(fā)生了些什么,為什么調(diào)用之后就能獲取 View 的寬高符匾?

  • 了解 MeasureSpec
    measure(int widthMeasureSpec叨咖,int heightMeasureSpec) 的參數(shù)是兩個(gè)符合 MeasureSpec 規(guī)范的 int 值。
    MeasureSpec 代表一個(gè)32位的 int 值啊胶,高2位代表 SpecMode甸各,低30位代表 SpecSize.
  • SpecMode
    測量模式,有以下三類焰坪。

UNSPECIFIED
EXACTLY
AT_MOST

  • SpecSize
    對(duì)應(yīng)測量模式下規(guī)格的大小趣倾。

  • 生成 MeasureSpec
    一組 SpecMode 和 SpecSize 可以打包成一個(gè) MeasureSpec:

public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}
  • 獲取 SpecMode 和 SpecSize
    一個(gè) MeasureSpec 同樣可以解包為一組 SpecMode 和 SpecSize:
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK);
}
  • 0所對(duì)應(yīng)的 MeasureSpec
    現(xiàn)在知道了傳遞的0并不是簡單的一個(gè)0,它符合著 MeasureSpec 規(guī)范某饰。
    將0解包后儒恋,所對(duì)應(yīng)的 SpecMode = 0,SpecSize = 0.
    SpecMode 0 對(duì)應(yīng)的模式為 UNSPECIFIED.
    UNSPECIFIED的官方解釋

The parent has not imposed any constraint on the child. It can be whatever size it wants.
父容器不會(huì)對(duì)子元素加以任何約束黔漂,子元素可以是任何大小诫尽。

  • 創(chuàng)建一個(gè)簡單的項(xiàng)目
//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);
    }
}
  • 代碼分析
    有了項(xiàng)目以后,在 LogImageView#onMeasure()#super.onMeasure() 處打上斷點(diǎn)炬守。
    觀察調(diào)用棧:


    調(diào)用棧

    從調(diào)用棧中可以看到當(dāng)調(diào)用 imgView.measure(0, 0) 時(shí)牧嫉,執(zhí)行了繼承自父類 View 的 measure 方法,measure 方法中調(diào)用了 LogImageView 重寫過的 onMeasure 方法劳较,打印log如下:

I/LogImageView: widthMeasureSize = 0
I/LogImageView: widthMeasureMode = 0
I/LogImageView: heightMeasureSize = 0
I/LogImageView: heightMeasureMode = 0

接著會(huì)執(zhí)行 super.onMeasure驹止,在 ImageView 的 onMeasure 中進(jìn)行實(shí)際的測量浩聋。
ImageView 的 onMeasure 方法比較長,進(jìn)行了刪減臊恋,只描述一下大概邏輯:

@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 的寬高衣洁,接著調(diào)用 getSuggestedMinimumWidth/Height 方法取較大值重新賦給寬高。

  • 分析 getSuggestedMinimumWidth:
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

如果 view 沒有 background 則返回最小寬度抖仅,否則比較 view 的最小寬度和 background 的最小寬度返回較大值坊夫。
view 的最小寬度 MinWidth 就是在 xml 中定義 android:minWidth 的值,或者是通過調(diào)用 view.setMinimumWidth 設(shè)置的最小寬度撤卢。
getSuggestedMinimumHeight 方法同理环凿。

然后通過 resolveSizeAndState 方法計(jì)算 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解包后對(duì)應(yīng)的 SpecMode 為 UNSPECIFIED放吩。
可以看到 specMode 為 UNSPECIFIED 時(shí)返回值 result 直接等于了 size智听,而在 EXACTLY 和 AT_MOST 情況中受到了 SpecSize 的影響,這也解釋了官方定義中說 UNSPECIFIED 模式下父容器不會(huì)對(duì)子元素加以任何約束的原因渡紫。
函數(shù)結(jié)尾 result | (childMeasuredState & MEASURED_STATE_MASK)到推,childMeasuredState 傳遞進(jìn)來為0,和 MEASURED_STATE_MASK 與運(yùn)算后結(jié)果為0惕澎,result 和0進(jìn)行或運(yùn)算保持不變莉测。
所以最后的 return 值就是傳遞進(jìn)來的 size。

最終調(diào)用父類 View 的 setMeasuredDimension 方法將計(jì)算出的 widthSize 和 heightSize 傳遞到 View 中唧喉。
至此 ImageView 的 onMeasure 方法分析完畢捣卤。
接下來在 View 內(nèi)繼續(xù)分析:

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 方法中參數(shù)最終傳遞給 setMeasuredDimensionRaw 方法。
在這里八孝,經(jīng)過一系列計(jì)算的 measuredWidth 和 measuredHeight 賦給了成員變量 mMeasuredWidth 和 mMeasuredHeight董朝,然后將 mPrivateFlags 狀態(tài)位設(shè)置為 PFLAG_MEASURED_DIMENSION_SET.
至此,調(diào)用 view.measure(0,0) 之后的計(jì)算得出的寬高值已經(jīng)保存到成員變量中干跛。

  • 取寬高
    現(xiàn)在調(diào)用 getMeasuredWidth/Height 方法就已經(jīng)可以獲得測量后的寬高益涧。
public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

MEASURED_SIZE_MASK 的值為 0x00ffffff,和 mMeasuredWidth 進(jìn)行與運(yùn)算后驯鳖,可以將 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 進(jìn)行了或運(yùn)算记舆,MEASURED_STATE_TOO_SMALL 的值為 0x01000000.
也就是說 SpecSize 的高6位(MeasureSpec 的高3~8位)會(huì)記錄 STATE 信息鸽捻,所以要將高8位全置0。
最終在 Log 中可以看到取出的寬高值:

I/MainActivity: imageView MeasuredWidth = 144
I/MainActivity: imageView MeasuredHeight = 144

最后

寫本文的初衷是很久以前我就在使用 view.measure(0,0) 來獲取寬高,但一直不知道為什么御蒲,0是什么意思衣赶?傳遞1進(jìn)去行不行?終于決定自己分析一下這個(gè)困擾已久的問題厚满。
斷斷續(xù)續(xù)加起來大概6個(gè)小時(shí)把這篇文章寫完府瞄,希望對(duì)大家也有所幫助。
如果有分析不對(duì)的地方或是其他建議碘箍,歡迎留言探討遵馆,謝謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丰榴,一起剝皮案震驚了整個(gè)濱河市货邓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌四濒,老刑警劉巖换况,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異盗蟆,居然都是意外死亡复隆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門姆涩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人惭每,你說我怎么就攤上這事骨饿。” “怎么了台腥?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵宏赘,是天一觀的道長。 經(jīng)常有香客問我黎侈,道長察署,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任峻汉,我火速辦了婚禮贴汪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘休吠。我一直安慰自己扳埂,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布瘤礁。 她就那樣靜靜地躺著阳懂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上岩调,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天巷燥,我揣著相機(jī)與錄音,去河邊找鬼号枕。 笑死缰揪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的堕澄。 我是一名探鬼主播邀跃,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蛙紫!你這毒婦竟也來了拍屑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤坑傅,失蹤者是張志新(化名)和其女友劉穎僵驰,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體唁毒,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒜茴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浆西。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粉私。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖近零,靈堂內(nèi)的尸體忽然破棺而出诺核,到底是詐尸還是另有隱情,我是刑警寧澤久信,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布窖杀,位于F島的核電站,受9級(jí)特大地震影響裙士,放射性物質(zhì)發(fā)生泄漏入客。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一腿椎、第九天 我趴在偏房一處隱蔽的房頂上張望桌硫。 院中可真熱鬧,春花似錦啃炸、人聲如沸鞍泉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咖驮。三九已至边器,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間托修,已是汗流浹背忘巧。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留睦刃,地道東北人砚嘴。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像涩拙,于是被迫代替她去往敵國和親际长。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容