在上一篇文章中,我們介紹了DecorView
與MeasureSpec
, 下面的文章就開始討論View的三大流程。
View的三大流程都是通過ViewRoot
來完成的。ViewRoot
對應於ViewRootImpl
類,它是連接WindowManager
與DecorView
的紐帶。在ActivityThread中,當Activity
對象被創建完畢之后,會將DecorView
添加到Window
中,同時創建ViewRootImpl
對象,並將ViewRootImpl
對象和DecorView
建立關聯。
View的繪制流程是從ViewRootImpl#performTraversals()
開始的,它經過measure(測量),layout(布局),draw(繪制)三個過程才能最終將一個view顯示出來。
- measure過程測量View的寬高尺寸。Measure完成以后,就可以通過
getMeasuredWidth()
和getMeasuredHeight()
來獲取View的寬高了。 - layout過程確定View在父容器中的擺放位置。layout()完成之后,就可以通過
getTop()
,getLeft()
,getRight()
,getBottom()
來拿到View的左上角,右下角的坐標數據了。 - draw過程負責將View繪制在屏幕上。draw()方法完成之后,View才能顯示在屏幕上。
ViewRootImpl#performTraversals()
依次調用performMeasure
, performLayout
,performDraw
三個方法。這三個方法依次完成View的 measure,layout,draw過程。
借用網上的一張圖,可以清晰的表達這個過程。
performMeasure
調用View#measure
方法,而View#measure
則又調用onMeasure
方法,而對於View中的onMeasure
方法,直接保存了測量得到的尺寸,而類似FrameLayout
,RelativeLayout
等各種容器ViewGroup,則在自己覆蓋重寫的的onMeasure
方法中,對所有的子元素進行measure過程,此時measure流程就從父容器傳遞到子View中了,這就完成了一次measure過程,最終這個遞歸的過程完成了整個View樹的遍歷。performLayout
,performDraw
的傳遞流程也是類似的。唯一不同的是performDraw
的傳遞過程是通過draw
方法中通過dispatchDraw
來實現的,但是道理都是相同的。- traversals的意思就是遍歷。
其實到了這個時候,我們大概的流程就已經知道了,那么剩下的具體就看看View和ViewGroup當中的具體的測量過程了。這兩個還要分情況討論,因為View是屬於自己一人吃飽,全家不餓,把自己測量好就行了,可是ViewGroup不僅要測量自己,還要遍歷去調用所有子元素的measure過程,從而形成一個遞歸過程。
而measure的大流程則如下:
測量的時候父控件的
onMeasure
方法會遍歷他所有的子控件,挨個調用子控件的measure方法,measure方法會調用onMeasure,然后會調用setMeasureDimension方法保存測量的大小,一次遍歷下來,第一個子控件以及這個子控件中的所有孫控件都會完成測量工作;然后開始測量第二個子控件…;最后父控件所有的子控件都完成測量以后會調用setMeasureDimension方法保存自己的測量大小。值得注意的是,這個過程有可能重復執行多次,因為有的時候,一輪測量下來,父控件發現某一個子控件的尺寸不符合要求,就會重新測量一遍。
View的measure過程
View的測量過程由measure()
來完成,measure()
方法是final
的,子類無法重寫覆蓋。它會調用onMeasure
方法。
根據
measure
的源碼,View其實也是比較喜歡偷懶的。意思是執行了measure
方法並不一定將測量過程完整走一遍(就是調用onMeasure
方法)。具體來說,如果View發現不是強制測量,且本次傳入的MeasureSpec
與上次傳入的相同,那么View就沒必要重新測量一遍。如果真的需要測量,View也先查看之前是否緩存過相應的計算結果,如果有緩存,直接保存計算結果,就不用再調用onMeasure
了。這樣也是最大限度的提高效率,避免重復工作。
onMeasure
方法代碼如下:
//View#onMeasure
/**
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
首先需要注意,該方法注釋的一句話,
This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents.
這就意味着,我們在進行自定義View時,是應該重寫覆蓋這個方法的。
先看一下onMeasure
方法中調用的getDefaultSize
方法。
//View#getDefaultSize
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
UNSPECIFIED
模式一般用於系統內部的測量過程,在這種情況下,View的大小為getDefaultSize
的第一個參數size,即寬度為getSuggestedMinimumWidth()
返回的值,而getSuggestedMinimumWidth()
可以用一句偽代碼來表示。
mMinWidth = android:minWidth;
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
對於測量過程而言,width,height過程都是一樣的流程。所以為了行文簡單,所以我們就簡單的只說width。
再回過頭來看 getDefaultSize
方法的源碼。可以看到,不管我們的View的LayoutParams
設置的是 match_parent
或者 wrap_content
,它的最終尺寸都是 相同的。再結合上一篇文章末尾我們根據getChildMeasureSpec
方法而整理出來的那張表。
當View為wrap_content
時,它的size就是parentSize-padding,這和match_parent
時,size是一樣的。(雖然mode可能不一樣)。
結論就是: 對於View來講,使用wrap_content
和使用match_parent
效果是一樣的,它倆的默認size是相同的。
所以各個子View,(當然也包括我們extends View
,自定義的View),在測量階段,針對wrap_content
都要做相應的處理,否則使用 wrap_content
就和使用 match_parent
效果都是一樣的, 那就是默認填充滿父View剩下的空間。
而我們在自定義View時,針對wrap_content
情況一般處理方式是,在onMeasure
中,增加對MeasureSpec.AT_MOST
的判斷語句,結合具體業務場景或情況,設定一個默認值,或計算出一個值。
wrap_content
的意思就是 包裹內容,但是仔細思考一下,內容又是什么呢,具體到不同的子View場景,肯定有不同的意義,所以從這個角度來思考,作為繼承結構上最頂級,同時也是最抽象的View而言,wrap_cotent
和match_parent
默認尺寸一樣,也就有道理了。
其實普通View的onMeasure邏輯大同小異,基本都是測量自身內容和背景,然后根據父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據文字的長度,大小,行高,行寬,顯示方式,背景圖片,以及父View傳遞過來的模式和大小最終確定自身的大小.
在測量結束時,調用了setMeasuredDimension
來存儲測量得到的寬高值,該方法源碼當中,注釋是這樣的。
This method must be called by {@link #onMeasure(int, int)} to store the measured width and measured height. Failing to do so will trigger an exception at measurement time.
如果我們自定義View時,重寫了onMeasure
方法,那么就需要調用setMeasuredDimension
方法來保存結果,否則就會拋出異常。
ViewGroup的measure過程
ViewGroup比View復雜,因為它要遍歷去調用所有子元素的measure過程,各個子元素再遞歸去執行這個過程。正所謂領導都要能者多勞之,領導要干的工作也比較多。
ViewGroup是一個抽象類,它沒有重寫View的onMeasure
方法,這是合理的,因為各個不同的子容器(比如LinearLayout
, FrameLayout
,RelativeLayout
等)它們有它們特定的具體布局方式(比如如何擺放子View),所以ViewGroup沒辦法具體統一,onMeasure
的實現邏輯,都是在各個具體容器類中實現的。
但是ViewGroup當中,提供了三個測量子控件的方法。
/**
*遍歷ViewGroup中所有的子控件,調用measuireChild測量寬高
*/
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);
}
}
}
/**
* 測量某一個child的寬高
*/
//ViewGroup中方法。
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);
}
/**
* 測量某一個child的寬高,考慮margin值
*/
protected void measureChildWithMargins (View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//獲取子控件的寬高約束規則,相比於 measureChild方法,這里考慮了 lp.margin值
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp. leftMargin + lp.rightMargin
+ widthUsed, lp. width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp. topMargin + lp.bottomMargin
+ heightUsed, lp. height);
//測量子控件
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
望名知意,根據方法的命名我們就能知道每個方法的作用。measureChild
,或measureChildWithMargin
,它們的作用就是取出child的layoutParams
,然后再通過getChildMeasureSpec
來創建child的MeasureSpec
,然后再將MeasureSpec
傳遞給child進行measure。這樣就完成了一輪遞歸。
所以我們在上一篇博客中,着重介紹了
getChildMeasureSpec
方法,指出這個方法是很重要的。
measureChildWithMargin
和measureChild
的區別就是父控件是否支持margin屬性。
因為各個不同的容器(比如LinearLayout
,FrameLayout
,RelativeLayout
等),都有各自的measure方式,所以我們就挑選LinearLayout
來看一下它的measure流程。
//LinearLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
我們就選擇豎直方向的measure過程來分析。
源碼比較長,我們只看主流程。
//LinearLayout#measureVertical
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
// ....
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
系統會遍歷子元素,並對每個子元素執行measureChildBeforeLayout
方法,該方法內部會調用子元素的measure方法,這樣各個子元素就依次進入measure過程。系統通過mTotalLength
這個變量動態存儲LinearLayout在豎直方向上的高度,並且伴隨着每測量一個子元素,mTotalLength
則會逐步增加。增加的部分包括了子元素的高度以及子元素在豎直方向上的margin等。
而當所有子元素都測量完畢之后,LinearLayout則會測量自己的大小。
//LinearLayout#measureVertical
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
針對豎直方向的LinearLayout而言,它在水平方向上的測量過程遵循View的測量過程,而豎直方向則和View有所不同,具體來說,如果它的高度參數是具體數值或match_parent
,則測量過程和View一致,即高度為SpecSize;而如果高度參數采用的是wrap_content
,那么它的高度就是所有子元素所占用的高度總和,但是仍然不能超過它父容器的剩余空間。並且最終高度還要考慮豎直方向的padding。
具體可以參考以下源碼:
//LinearLayout
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
//當specMode為AT_MOST,並且父控件指定的尺寸specSize小於View自己想要的尺寸時,
//我們就會用掩碼MEASURED_STATE_TOO_SMALL向測量結果加入尺寸太小的標記
//這樣其父ViewGroup就可以通過該標記知道其給子View的尺寸太小了,
//然后可能分配更大一點的尺寸給子View
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
resolveSizeAndState
方法和getDefaultSize
方法類似,其內部實現邏輯是一樣的,不過區別在於,resolveSizeAndState
方法除了返回尺寸信息,還會返回測量的status標志位信息。
View的測量過程是三大流程中比較復雜的,只有測量完畢之后,我們才有可能得到正確的寬高值。
而View的measure過程和Activity的生命周期方法是不同步的。所以我們不能簡單的通過onStart
,onResume
方法來獲取View的尺寸值。
參考內容:
github: https://github.com/yaowen369
歡迎對於本人的博客內容批評指點,如果問題,可評論或郵件(yaowen369@gmail.com)聯系
<p >
歡迎轉載,轉載請注明出處.謝謝
</p>
<script type="text/javascript">
function Curgo()
{
window.open(window.location.href);
}
</script>