好久沒有寫文章了,最近公司社招窗口重新打開了,又忙着面試,在面試過程中發現自己已經有些不知道問候選人什麼問題了...大寫的尷尬。特別是現在很多同學準(各)備(種)充(背)分(書),通常我剛問請你描述一下Android
中View
的測量過程,候選人已經開始如長江流水滔滔不絕地背書,怎麼去甄別他們是真懂還是短時間突擊?短時間突擊不是不可以,我們需要的人才是真正能夠理解這個過程的人,知其然而且知其所以然,這樣在真正項目中遇到問題的時候,你才能快速定位到問題。基於此,我只好把這塊東西的源碼再過一遍,其實今天的這篇文章是我14年發表在公司內部wiki上面的博文,稍微整理一下,放出來跟大家分享一下吧。
如果你能回答出來如下的問題,那麼這篇文章可能對你沒有太大的幫助,你可以略過了。也歡迎大家在評論中提出自己的答案,我們可以一起討論討論。
- 自定義一個
ViewGroup
需不需要重寫onMeasure
?爲什麼? - 我們在一個
ViewGroup
容器中(比如LinearLayout
)加入一個View(android.view.View)
爲啥設置match_parent
和設置wrap_content
效果一樣? -
View.getWidth
和View.getMeasureWidth
的值在整個繪製流程中是否一樣?在繪製完成之後兩個值是否一樣? - 如果一個自定義
View
需要支持wrap_content
設置的值,那麼它需要做什麼? - 如果我給一個
View
設定了一個layout_width="100px"
,那麼是否在任何佈局裏面它都會展示成100個像素?
帶着這些問題,我們進入今天的正文:
1 測量流程主線
Android
中View
的測量是一個比較複雜的過程,但是在Android
中所有跟視圖樹相關的內容,請你記住一條原則,他們都是從根佈局開始,然後遍歷到葉子View
,抓住這根主線之後,我們來看看Android
中Measure
的過程吧。
首先,整個過程的開始都是在ViewRootImpl
中開始的,至於ViewRootImpl
是個什麼東東,它實際上是在Activity
和Window
中間的一個代理層,系統消息都是通過發送到ViewRootImpl
,來觸發整個視圖樹的響應,ok,你瞭解到這裏就行了,如果需要詳細知道系統消息具體是怎麼流轉到ViewRootImpl
,建議你在網上搜索一下吧,很多文章都是描述這個過程,ViewRootImpl
有個方法performTraversals()
,它是整個視圖樹進行繪製的入口,我們常說的繪製三大流程都是在這裏觸發的。
private void performTraversals() {
//省略代碼
//measure入口
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//省略代碼
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
//省略代碼
performDraw();
}
既然performMeasure
是入口,那麼它具體是怎麼做的呢?因爲ViewRootImpl
會持有Activity
的DecorView
,所以在performMeasure
中,它就會直接去調用DecorView
的measure
方法,我們知道DecorView
是整個Activity的視圖樹的根佈局,通常情況下它是一個FrameLayout
(所以它自然是一個ViewGroup
),所以這裏就開啓了從上往下的遍歷measure
過程。
注意,這篇文章基於的SDK版本可能是5.x,如果你是4.x或者更老的版本,
ViewRootImpl
裏面是沒有這裏所說的performMeasure performLayout performDraw
方法的,它是直接調用Decorview
的measure layout draw
方法,本質上沒有啥區別。
View
中和測量過程相關的方法有三個,measure
、onMeasure
和setMeasuredDimension
。相應的,View
的測量過程有三步:
- 由父
View
調用public final void measure(int widthMeasureSpec, int heightMeasureSpec)
,如果是最外層的DecorView
,我們前面已經說了它是通過ViewRootImpl
觸發的。這個方法定義成final
,表示Android
不希望開發者改變整個視圖樹的measure
流程。 -
measure
調用onMeasure(int widthMeasureSpec, int heightMeasureSpec)
,這裏是View
實現測量的核心邏輯,開發者可以重寫這個方法,達到修改view
的measure
效果的作用。ViewGroup
基本上肯定需要自定義這個方法。注意,在這個方法中必須調用setMeasuredDimension
,否則會報異常。
3、onMeasure
中必須要調用setMeasuredDimension(int measuredWidth, int measuredHeight)
,設置測量的結果。
根據前面說的,我們大概已經知道了視圖樹的整體測量流程:
下面我們再來看看整個測量過程中的具體的細節。
2 測量過程的細節
既然我們已經找到測量視圖樹的入口了,是不是就可以開始接着往下擼源代碼了呢?稍等,我們先來了解一下整個測量流程中一個非常重要的類:View.MeasureSpec
。
2.1 View.MeasureSpec
在測量過程中,你可以看到父View
和子View
之間的數據傳遞就是普通的int類型,比如measure(int widthMeasureSpec, int heightMeasureSpec)
的函數原型。在Android
中,這個int類型其實包含了兩部分信息:大小(specSize
)和模式(mode
),mode
指的是父View期望子View按照某種建議去測量,specSize
是具體的大小。其中高兩位表示mode、低三十位表示specSize。爲了避免我們自己去進行這些移位操作,Android
提供了一個工具類MeasureSpec
,可以方便的根據它去操作,生成一個包含mode
和specSize
的int值。
mode
有三種類型:
-
EXACTLY
:父View希望子View直接使用傳給子view
給的specSize
。(當然,子view
按不按這個來,具體子View的onMeasure
說了算) -
AT_MOST
:父View
希望子View
最多隻能是specSize
中指定的大小,子View
需要保證不會超過specSize
。 -
UNSPECIFIED
:父View對子View
沒有要求,你想怎麼來,看你自己的脾氣。
大家都知道,在Android
中View
其實並不是一個抽象類,也就是我們可以直接new
出來一些View
的實例,那麼View
肯定也處理了measure
過程,我們看看它是怎麼做的吧:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我們可以看到View
的默認實現很簡單,直接調用了setMeasuredDimension()
設置測量的結果。其中getSuggestedMinimumWidth
和getSuggestedMinimumHeight
都是我們通常給View設置的最小寬高,比如android:minWidth="23dp"
。我們接着來看看getDefaultSize()
這個函數,跟進去看看:
public static int getDefaultSize(int size, int measureSpec) {
//size 的值就是外面傳進來的最小值
int result = size;
//父View傳給子View的模式
int specMode = MeasureSpec.getMode(measureSpec);
//父View傳給子View的大小
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
// 代碼1
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
// 代碼2
result = specSize;
break;
}
return result;
}
我們看到View
默認實現的測量還是挺簡單的,代碼1處,如果父View
指定的Mode
是UNSPECIFIED
,View
直接返回它自己最小值。代碼2處,AT_MOST
和EXACTLY
都是直接返回父View
傳遞進來的值。
看到這裏,我相信你已經有點蒙逼了,最大的疑惑是父View
傳進來的Mode
和Size
是怎麼算的?下面我就來解決這個疑惑吧,我們把代碼切到ViewGroup
中來。
2.2 ViewGroup的Measure流程
我們注意到ViewGroup
它是一個抽象類,所以我們並不能直接new
一個ViewGroup
實例,那我們繼承一個試試:
public class MyView extends ViewGroup {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for(int i=0;i<getChildCount();i++) {
View child = getChildAt(i);
child.layout(l,t,child.getMeasuredWidth(),child.getMeasuredHeight());
}
}
}
這裏爲了演示方便,我們直接把MyView中所有的子View放到了左上角(onLayout中處理),xml中這樣指定:
<com.chuyun932.learn.view.MyView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff0000"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#00ff00"
android:text="測試"
android:id="@+id/test"
/>
</com.chuyun932.learn.view.MyView>
我們看一下頁面run起來的結果:
我們明明給TextView
設置了match_parent
和100dp
的高度,結果View
並沒有顯示到界面中間來,你能解釋爲什麼嗎?因爲MyView
並沒有重寫View
的onMeasure
,所以在View
的默認實現中,它只會去measure
自己(當前是MyView
),所有MyView
的子View
都得不到measure
的機會,所以他們的getMeasureWidth
都是0,那麼在Layout
階段我們依據measure
的值去佈局的時候,自然也就不會給它分配佈局空間了。
雖然ViewGroup
沒有實現omMeasure
的過程,但是它提供了兩個工具方法:measureChildren()
和getChildMeasureSpec()
。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
在measureChildren
中,ViewGroup
對每一個不爲GONE
的View
調用measureChild
:
protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChild
也很簡單,我們終於找到調用View
的measure
的入口了,參數產生的位置就在getChildMeasureSpec
中,所以這就是我們今天這篇文章的核心內容啦,我們單獨起一節來說它。
2.3 getChildMeasureSpec生成參數
我們在一開始就說了,在視圖樹從根開始進行遍歷的過程中,傳遞的參數就是int類型的變量,它有兩個含義,mode
和大小,那我們下面來看看ViewGroup
中提供的工具方法是如何產生給子View
的參數的吧。
首先,getChildMeasureSpec()
的輸入就很有意思,第一個參數是外面傳遞給當前這個ViewGroup
的參數;第二個參數是當前ViewGroup
的padding
值,第三個參數是子View
的layoutparams.layout_height
和layoutparams.layout_width
。
我們設置
layout_height
的方式一共有三種,match_parent
、wrap_content
和直接給一個值。match_parent
和warp_content
都是一個負值,所以我們判斷第三個參數是否 > 0,就可以知道子View是否設定了一個確切的值。在ViewGroup
實現的時候,這三種方式其實就會影響測量流程中的MODE
。
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//拿到ViewGroup的父View傳遞進來的mode和size,其實就是當前ViewGroup的measure參數
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY:
//如果子View設置了一個確定值
if (childDimension >= 0) {
//直接給它確切值,模式是EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//直接給它當前ViewGroup的大小,模式是EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 代碼1 如果子View設置的是wrap_content,那麼把當前GroupView的大小給它,然後告訴它最大是這麼多了
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
//如果外面傳給ViewGroup的mode是給最大值
case MeasureSpec.AT_MOST:
//如果子View設置了一個確定值,那麼還是直接給子View它期望的值
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//告訴子view你最大也就這麼大
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 父View讓我們自己決定你有多大
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0)
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
上面這個方法很長,但是我們能看出來一些規律:
1、 如果子View
設置了layout_width(height)
,那麼一般情況下ViewGroup
會直接按照它需要的寬高設置spec
大小,同時mode
爲EXACTLY
,也就是說對於一個行爲良好的ViewGroup
,它不應該去改變這個約定。但是。。。不應該!=不能。
2、如果子View
設置了我們設置了layout_width(height)="wrap_content"
,那麼子傳遞給子View
的就是當前ViewGroup
的大小,同時指定mode
爲AT_MOST
,告訴子View
你自己去measure
你自己,但是不能超過我的大小。
3、什麼時候用UNSPECIFIED
?
要說清這個事情,我先賣個關子,我們先來說明另外一個東東。堅持看到這裏的你有沒有一個疑問,最頂層的DecorView measure()
方法的參數是誰傳遞給它的?是什麼?
2.4 DecorView 測量入口參數
根據前面的分析,你要找這個入口上哪裏看代碼?沒錯,就是ViewRootImpl
中去,我們看到執行視圖樹的measure
過程的函數其實也是接收兩個int參數:
childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //直接調用DecorView的measure方法!!入口
baseSize
和 desiredWindowHeigh
t參數其實是當前Window
的大小,lp
在這裏是Window.LayoutParams
,是Activity
設置的Window
的LayoutParams
,當然一般情況下都是match_parent
。那我們看看getRootMeasureSpec
做了什麼:
private int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
一般Activity
都是設置的Window
的屬性都是match_parent
,那麼這裏傳遞給DecorView
的參數就是 window
的大小+ EXACTLY
的mode
。
這個時候,我們再來看看UNSPECIFIED
的話題,你會發現在整個視圖樹測量中,正常情況下我們完全走不到UNSPECIFIED
這個分支,爲什麼?因爲頂層傳入的mode
就是EXACTLY
,ViewGroup
默認實現在傳遞給子View
的時候,只有外面傳給自己是UNSPECIFIED
的時候,它纔會傳遞UNSPECIFIED
給子View
。那爲什麼存在UNSPECIFIED
這個模式呢?
從ViewGroup的角度來看,如果一個子View設置了match_parent
和wrap_content
,前者我直接吧自己的大小傳遞給子View
,並指定mode
爲EXACTLY
;後者我還是把自己的大小傳給子View
,並告訴它你最大不能超過我這個值。除了這兩種場景,你想想還有別使用場景嗎?
比如在ScrollView
中,ScrollView
能包含一個LinearLayout
的子View
,這個時候其實LinearLayout
在measure
自己的時候,其實就不需要參考父View
的大小,所以ScrollView
會給它的子View
的mode
設置成UNSPECIFIED
。
2.5 View的Measure過程
我們前面說了這麼多,主要解析了ViewGroup
傳遞參數給子View
,那麼子View
拿到這個參數之後,就會去走自己的onMeasure
,所以父View
和子View
的測量其實是協商的過程,父View
給你建議了,子View
怎麼實現?當然最好是按照父View
的建議來測量唄,我們來舉個反例吧:
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(100,100);
}
}
這是個傲嬌的View
,它在自己的onMeasure
方法中直接設置了自己的measure
結果,直接忽略了父View
給的建議,這樣的後果是什麼?你在xml
中給MyView
設置的layout_width layout_height
屬性都完全失效,比如:你設置了Layout_width="50px"
,父View
調用MyView
測量的時候,它看到設置了layout_width="50px"
,那麼父View
傳遞給MyView
的measure
參數肯定是:mode
爲EXACTLY
,specSize
爲50
,但是MyView
在自己的onMeasure
裏面壓根就不考慮父View的建議,所以所有給它設置的Layout_width
和height
都是無效的。
總結
你可以看到Android
中將一個View
展示到頁面上是一件多麼複雜的過程,measure
只是萬里長征第一步。其實在Andriod
中,視圖樹的很多通知和操作都是基於父View
和子View
協商完成的,測量過程也是如此,後面有時間我會整理一下Layout
和Draw
過程,敬請期待。