在 Activity 的 onCreate季惯、onStart吠各、OnResume 生命周期中,無法直接得到 View 的寬高信息勉抓。
網(wǎng)上有以下幾種常見的解決辦法:
- 在 Activity#onWindowFocusChanged 回調(diào)中獲取寬高走孽。
- view.post(runnable),在 runnable 中獲取寬高琳状。
- ViewTreeObserver 添加 OnGlobalLayoutListener,在 onGlobalLayout 回調(diào)中獲取寬高盒齿。
- 調(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ì)的地方或是其他建議碘箍,歡迎留言探討遵馆,謝謝。