本篇文章來分析一下View的測量流程,在寫自定義控件時往往需要重寫View的onMeasure來定義自己的測量規矩,如果我們不熟悉測量流程,就算根據別人的自定義控件學習,最後寫出來了,但是再遇到新的自定義控件可能還是無從下手
我們都知道View的測量,佈局,繪製的入口在ViewRootImpl的performTraversals方法開始,而View的測量則是從最頂層的DecorView開始的,先測量子View,在測量父View,測量的規則其實就是根據父View的MeasureSpec和子View的LayoutParams再根據一個特定規則算出子View的大小
MeasureSpec
MeasureSpec中包含一個View的測量模式和size,這個size並不一定是最終View的大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
測量模式有三種:
UNSPECIFIED: 不限制View的大小,如ListView,scrollView,無法進行限制
EXACTLY: 精確模式,如在xml中寫死View的大小或者在父View模式爲EXACTLY時自己在xml中寫的match_parent
AT_MOST: 在xml中寫的wrap_content或者在父View模式爲AT_MOST時自己在xml中寫的match_parent
我們前面說了測量一個View是用的父View的MeasureSpec和子View的LayoutParams來計算的,那麼最頂層的DecorView沒有父View,它的MeasureSpec哪來的呢?
我們從測量的入口來看看DecorView的MeasureSpec的獲取
performTraversals
private void performTraversals() {
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
......
}
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
}
}
在ViewRootImpl的performTraversals方法中調用performMeasure開始測量,performMeasure中調用mView的measure方法,這裏的mView就是DecorView,我們來看看childWidthMeasureSpec和childHeightMeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
childWidthMeasureSpec和childHeightMeasureSpec的獲取是通過調用getRootMeasureSpec方法,傳遞了mWidth,mHeight以及lp.width,lp.height,lp是在ViewRootImpl中創建的LayoutParams,調用了WindowManager.LayoutParams()的無參構造函數,默認值是:MATCH_PARENT
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
而mWidth和mHeight則是手機屏幕的寬高,再看看getRootMeasureSpec方法
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
getRootMeasureSpec方法中合成了DecoreView的MeasureSpec,根據傳遞的lp.width,lp.height爲MATCH_PARENT得到傳遞到DecorView的MeasureSpec模式爲EXACTLY,size爲手機屏幕的寬高,接着看performMeasure中調用的mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
DecoreView是一個FrameLayout,所以會調用FrameLayout的measure方法,而measure方法定義在View中,是final修飾的,所以子類無法繼承,實際上View的measure方法中調用了onMeasure,這個方法是給子類實現自己的測量規則的
FrameLayout.onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
......
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//mMeasureAllChildren代表是否測量所有View
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
...
}
}
......
}
遍歷DecoreView中的所有子View,如果mMeasureAllChildren爲true或者當前子View不是Gone狀態則進一步調用measureChildWithMargins測量子View,到這裏我們先說一下DecoreView的內部佈局,DecoreView內部會在PhoneWindow的setContentView方法中創建DecoreView時加載一個如下xml佈局,不考慮任何樣式的情況下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
所以可以簡單認爲DecoreView中包含一個LinearLayout,這個LinearLayout中包含一個actionbar和一個id爲content的FrameLayout,通過setContentView填充的就是這個FrameLayout,忽略actionbar的測量
好了,瞭解了DecoreView內部佈局後我們再回到其onMeasure方法中,遍歷DecoreView的子View,調用measureChildWithMargins開始測量,這裏子View就是這個LinearLayout
measureChildWithMargins
此方法定義在ViewGroup中
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//(1)
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//(2)
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//(3)
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//(4)
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
我們分幾步來看看,首先:
(1)獲取LinearLayout的LayoutParams,獲取LayoutParams的目的其實是需要獲取xml中定義的Margin以及layout_width,layout_height
(2)(3)分別計算LinearLayout寬高的MeasureSpec,計算MeasureSpec時需要父View的MeasureSpec和父View的Padding和自己的Margin再加上一個widthUsed或者heightUsed,被其他View使用過的空間
(4)得到了LinearLayout的MeasureSpec之後會傳遞給內部的子類,子類再進行測量
getChildMeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//獲取父View的測量模式
int specMode = MeasureSpec.getMode(spec);
//獲取父View的size
int specSize = MeasureSpec.getSize(spec);
//由父View的size減去父View的padding和自己的margin以及Used
//得到最終size(爲了方便後面依然統稱爲父View的size)
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//根據父View的測量模式以及自己LayoutParams計算MeasureSpec
switch (specMode) {
// 如果父View的模式爲EXACTLY
case MeasureSpec.EXACTLY:
//如果自己的xml中寫的值是一個不是match_parent或者wrap_content
if (childDimension >= 0) {
//則自己的size就是xml中寫的值
resultSize = childDimension;
//自己的模式就是精確的
resultMode = MeasureSpec.EXACTLY;
//如果自己的xml中寫的是MATCH_PARENT
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//則自己的size就是父View的size
resultSize = size;
//自己的模式就是精確的,因爲父View的模式是精確的
resultMode = MeasureSpec.EXACTLY;
//如果自己在xml中寫的是WRAP_CONTENT
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//則自己的size暫時定爲父View的size,因爲此時還沒有
//對自己進行具體測量,無法得到具體值
resultSize = size;
//自己的模式爲AT_MOST,因爲就算還沒有具體測量但最終
//肯定不能超過父View的大小
resultMode = MeasureSpec.AT_MOST;
}
break;
//父View的測量模式爲AT_MOST,即父View此時也不能確定自己的size
case MeasureSpec.AT_MOST:
//如果自己的xml中寫的值是一個不是match_parent或者wrap_content
if (childDimension >= 0) {
//則自己的size就是xml中寫的值
resultSize = childDimension;
//自己的模式爲EXACTLY
resultMode = MeasureSpec.EXACTLY;
//如果自己的xml中寫的是MATCH_PARENT
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//則自己的size就是父View的size
resultSize = size;
//自己的模式爲AT_MOST,因爲父View都不能確定自己大小
resultMode = MeasureSpec.AT_MOST;
//如果自己的xml中寫的是WRAP_CONTENT
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//則自己的size暫定爲父View的size
resultSize = size;
//自己的模式爲AT_MOST,自己雖然爲WRAP_CONTENT,
//但最終還是不能超多父View大小
resultMode = MeasureSpec.AT_MOST;
}
break;
//如果父View模式爲UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
//如果自己的xml中寫的值是一個不是match_parent或者wrap_content
if (childDimension >= 0) {
//則自己的size就是xml中寫的值
resultSize = childDimension;
//自己的模式爲EXACTLY
resultMode = MeasureSpec.EXACTLY;
//如果自己的xml中寫的是MATCH_PARENT
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//則自己的size取決與sUseZeroUnspecifiedMeasureSpec,
//這個值在View中初始化,當sdk版本小於M時爲true,大於爲false
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
//自己的模式爲UNSPECIFIED
resultMode = MeasureSpec.UNSPECIFIED;
//如果自己的xml中寫的是WRAP_CONTENT
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//則自己的size取決與sUseZeroUnspecifiedMeasureSpec,
//這個值在View中初始化,當sdk版本小於M時爲true,大於爲false
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
//自己的模式爲UNSPECIFIED
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//根據計算出來的size和mode生成自己的MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
getChildMeasureSpec這個方法看着很多代碼,其實仔細看,邏輯很清晰,只要記住自己的MeasureSpec一定是根據父View的MeasureSpec和自己的LayoutParams來計算的,是兩個條件共同決定,所以有很多判斷,大致對這些判斷進行分類其實有三個大分支:
- 父View的測量模式爲EXACTLY時
子View只有在xml中寫了WRAP_CONTENT時才需要在自己onMeasure方 法中定義測量規則 - 父View的測量模式爲AT_MOST時
子View在xml寫了WRAP_CONTENT和MATCH_PARENT時都需要在自己onMeasure方法中定義測量規則 - 父View的測量模式爲UNSPECIFIED時
不用管,這一般是系統的View
總結:View需要測量的情況其實就是自己的mode計算出來爲AT_MOST時,如果是EXACTLY的模式,根本不需要我們定義測量規則
好了由DecoreView的MeasureSpec就計算出了它裏面LinearLayout的MeasureSpec了,這個LinearLayout的layout_width和layout_height是match_parent,根據上面分析的測量規則,所以這個LinearLayout的mode就是EXACTLY,size爲DecoreView的size減去DecoreView內部padding和自己的margin
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
得到了MeasureSpec之後,measureChildWithMargins方法的(4)步會調用LinearLayout的measure方法繼續進行測量,前面已經說過measure這個方法定義在View中,不可繼承,實際上是在measure中調用子類onMeasure方法
LinearLayout.onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
這裏分爲垂直佈局和水平佈局,隨便看一個
measureHorizontal
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
//useExcessSpace代表是否使用權重的計算方式
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
...
} else {
...
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
}
}
同樣是對LinearLayout內部的View進行遍歷,如果不是採用權重的方式計算則調用measureChildBeforeLayout方法測量LinearLayout的子View,measureChildBeforeLayout方法中同樣是調用了我們前面分析的measureChildWithMargins來測量,傳遞的是LinearLayout的MeasureSpec,我們可以發現其實是按一種遞歸的方法,從最頂層的DecoreView到LinearLayout到裏面的子View,如果繼續還是ViewGroup,可以想到,肯定還是遍歷其子View調用measureChildWithMargins,直到最終找到非ViewGroup的View,調用其onMeasure方法,此時的onMeasure肯定就不會遍歷子View了,而是實實在在的測量自己,當所以子View測量完成之後,再對那些xml中寫的是WRAP_CONTENT的父View進行進一步測量,所以我們前面說measureChildWithMargins測量的size並不一定就是最終View的size,
然後按不同的規則,比如LinearLayout可能是子View的寬或者高累加,FrameLayout可能是按它裏面最大的View的寬高得到的,這就需要去看它的源碼如何定義的規則
我們在自定義View時,重寫onMeasure方法時,它裏面的widthMeasureSpec和heightMeasureSpec其實就是通過ViewRootImpl的performMeasure方法開始,從DecoreView開始測量,計算的MeasureSpec一步一步,慢慢計算傳遞過來的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
說白了,測量流程其實就是爲了給那些mode爲AT_MOST的自定義View規定一個測量規則好計算自己的size,mode爲AT_MOST時並不是一個準確的值,只是暫時的值,所以需要你重寫onMeasure方法,告訴系統,你的View的測量規則,對於那些自定義View沒有重寫onMeasure方法的View,Android提供了一個默認的測量方法,我們來看下
View.onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//設置View的寬高,此方法調了之後,測量流程就結束了
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
先看看getDefaultSize方法
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
//獲取自己的測量模式
int specMode = MeasureSpec.getMode(measureSpec);
//獲取自己MeasureSpec的size
int specSize = MeasureSpec.getSize(measureSpec);
/*
只要自己的模式不爲UNSPECIFIED,則自己的寬高就等於
MeasureSpec保存的寬高
*/
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
所以對一般的自定義View來說,如果你不重寫onMeasure方法就默認使用MeasureSpec的size,而MeasureSpec的size根據我們前面getChildMeasureSpec方法的分析,不重寫onMeasure情況下,WRAP_CONTENT等價於MATCH_PARENT
總結一下View的測量過程:
- View的測量是從ViewRootImpl的performMeasure爲入口,首先會根據手機屏幕的寬高和默認值MATCH_PARENT給DecoreView創建一個MeasureSpec
- 接着調用DecoreView的measure實際調用FrameLayout的onMeasure方法進行測量,onMeasure中會遍歷子類View,以此調用measureChildWithMargins測量子View
- 接着又會調用id爲content的FrameLayout的onMeasure方法測量其子View,所以View的測量其實就是一個遞歸的過程
- measureChildWithMargins測量子View其實就是根據父View的MeasureSpec和子View自己的LayoutParams計算得到子View自己的MeasureSpec,這裏麪包含了子View的測量模式和暫時的size
- 自定義View或者自定義ViewGroup時一定要重寫onMeasure方法,只要你打算在xml中使用WRAP_CONTENT
分析完整個View測量流程之後我們再來聊聊爲什麼Activity的onCreate方法中獲取不到View的寬高,我們能想到要獲取View寬高一定要等View測量完成纔行,畢竟沒有測量完誰知道大小呢,所以我們能夠進一步想到onCreate方法一定是在View的測量之前執行的,就是這個原因,Activity執行onCreate回調時View的測量還沒開始呢
View的測量是在Activity的onResume回調纔開始執行的
後面會再寫一篇UI刷新機制詳細分析View測量流程是怎麼開始的