一、前言
眾所周知,android為我們提供大量的基礎控制元件,這些控制元件完成基本功能是沒有問題的,也比較全面,但是對於一些比較精緻的產品,不僅僅是基礎功能實現就OK,它們往往要很炫的效果,這就需要自定義view了,好了不多說了,直接開始主題,View的繪製分為measure、layout、draw,其中測量是最複雜的,我們單獨來講,佈局和繪製將在下一篇文章去講解。
二、理解ViewRoot和DecorView
在正式講解View的工作原理之前,我們先了解一下ViewRoot,ViewRoot的實現類是ViewRootImpl,它是連線WindowManager和DecorView的紐帶,View的三大流程都是通過ViewRoot來完成的,它是在ActivityThread中被初始化的。View的繪製流程是從ViewRoot的performTraversals開始的,經歷三個步驟後最終呈現在介面的view,大致如下:
performTraversals會依次呼叫perfornMeasure、performLayout、performDraw,這三個分別完成頂級View的measure、layout、draw,performMeasure再去呼叫measure,最後去呼叫onMeasure完成子view的測量,子view會再去呼叫measure,依次遞迴下去,直到所以的子view measure完畢。下面來簡單講一下DecorView,如下圖:
DecorView是所有View的頂級View,它裡面有個LinearLayout,分為title bar和content,我們經常在onCreat裡面用到的setContentView方法就是為這個content設定佈局的,也就是說,我們寫的佈局都塞進了這個content,哦。。。。,明白了,這就是為啥要叫setContentView而不叫setView了吧。三、理解一下MeasureSpec
3.1 MeasureSpec的概念
可以說,這個概念是貫穿了整個View繪製的所有流程,是的,表面上看它就是一個尺寸規格,也就是決定View的大小,它絕大部分都可以決定View的大小,當然也不是它一個人說了算,畢竟有些ViewGroup的LayoutParams也對子view的大小有影響。
MeasureSpec代表一個32位的int值,其中高兩位是mode,低30位是size,其中mode的三種值,分別是:- UNSPECIFIED:父容器不對View做任何限制,要多大就給多大,這種主要用於系統內部,應用層開發一般用不到。
- AT_MOST:就是子View的值根據自己定義的大小來給定,但是不可以超過父類的大小,相當於LayoutParams的wrap_content。
- EXACTLY:父類已經檢測到了子View的精確大小了,這時候View的大小就是SpecSize,它對應LayoutParams的match_parent和具體值這兩種情況。
3.2 MeasureSpec和LayoutParams的對應關係
上面提到過了,View的大小是由MeasureSpec來決定的,我們一般會給view設定LayoutParams引數,這個params引數會在父容器的MeasureSpec約束的情況下轉換為對應的MeasureSpec,這個Spec會最終確定view的測量大小,也就是說view的大小是由父容器的MeasureSpec和view的LayoutParams共同決定的,MeasureSpec一旦確定後,onMeasure就可以確定view的測量寬高了。
View的measure過程是由ViewGroup的measure傳遞來的,這裡看一下ViewGroup的measureChildWithMargins方法,
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
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);
}
複製程式碼
可以看到,在測量子view的時候會去獲取子view的MeasureSpec,這裡詳細看一下getChildMeasureSpec方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
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) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製程式碼
這個方法有點長,但是很簡單,它就是根據父容器的MeasureSpec和子view的LayoutParams來確定子view的MeasureSpec。我們把上述規則總結到一張表裡面,方便記憶:
這裡不是我自己創造的一張表,而僅僅是對上述過程的一種解釋而已。但是有一種特殊的情況我們要注意一下, 就是當子view是warp_content的時候,不管父類是啥,結果都是一樣的,這就會有問題,怎麼辦呢,這就交給子view的onMeasure去處理吧,所以在自定義view的時候如果view 的params設定為wrap_content的時候,我們就要去實現onMeasure方法。具體的後面會講。四、View的measure過程
view的三大過程中,measure是最複雜的,因為往往要確定一個view的大小,要經歷好多次測量才能ok。measure過程要分情況來看,View和ViewGroup,因為ViewGroup不僅僅要測量自己還要測量子元素,一層一層傳遞下去。
4.1、單個View的measure
View裡面的measure是final方法,這就意味著該類不允許被繼承,measure裡面呼叫了onMeasure方法,也就是說measure的工作就是在onMeasure裡面完成的,看看o n M e asure方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製程式碼
程式碼很簡單,setMeasuredDimension設定測量的值,主要的是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;
}
複製程式碼
getDefaultsize就是返回測量後的大小,這裡注意是測量後的大小,因為view的大小最終確定是在layout後,有時候layout也會對view的大小造成影響,不過絕大部分getDefaultsize就是最終view的大小。
注意的點: 從getDefaultsize方法可以看到,view的大小由specSize來決定,所以,直接繼承View的自定義控制元件需要重寫onMeasure方法並且設定wrap_content時的自身大小,否則在佈局中的wrap_content和match_parent就沒有什麼區別了,從上面的表格也可以清晰的看到,這種情況是我們不希望看法的,怎麼解決呢?很簡單,我們設定一個預設值就可以了
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//如果view在佈局中使用wrap_content ,這時候就是AT_MOST,我們需要在onmeasure裡面做特殊處理,否則和match_parent就沒有區別了
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(500, 300);
}else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, 300);
}else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(500, heightSpecSize);
}
}
複製程式碼
具體的預設值是多少,我們根據自己的情況來定。
4.2 ViewGroup的measure過程
對於ViewGroup的measure過程,它會更加複雜一點,因為它不僅要measure自己,還要measure子view,ViewGroup沒有重寫onMeasure方法,它提供了另一種方法measureChildren
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);
}
}
}
複製程式碼
上面的方法很明瞭,就是對每一個子元素進行measure,我們看看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);
}
複製程式碼
這也太簡單了,就是獲取子元素的measure spec值,然後呼叫view的measure操作,這個和單獨的view就沒啥區別了,就這樣一直迭代下去,直到單個的view測量結束。這是測量子元素的過程,那麼ViewGroup怎麼測量自己的呢。
其實ViewGroup並沒有定義其測量的具體過程,因為它是一個抽象類,其測量過程onMeasure交給了其子類去實現了,比如LinearLayout類就有自己專門的onMeasure方法,這也是符合邏輯的,因為沒個Layout都有自己的特性,我們不可能在ViewGroup統一去處理。 我們以LinearLayout為例,看如下程式碼:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
複製程式碼
看垂直方向的,水平方向的類似:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0;
...
final int count = getVirtualChildCount();
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
boolean matchWidth = false;
boolean skippedMeasure = false;
final int baselineChildIndex = mBaselineAlignedChildIndex;
final boolean useLargestChild = mUseLargestChild;
int largestChildHeight = Integer.MIN_VALUE;
int consumedExcessSpace = 0;
int nonSkippedChildCount = 0;
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
if (child == null) {
mTotalLength += measureNullChild(i);
continue;
}
if (child.getVisibility() == View.GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
nonSkippedChildCount++;
if (hasDividerBeforeChildAt(i)) {
mTotalLength += mDividerHeight;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
final int margin = lp.leftMargin + lp.rightMargin;
final int measuredWidth = child.getMeasuredWidth() + margin;
maxWidth = Math.max(maxWidth, measuredWidth);
childState = combineMeasuredStates(childState, child.getMeasuredState());
allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
if (lp.weight > 0) {
/*
* Widths of weighted Views are bogus if we end up
* remeasuring, so keep them separate.
*/
weightedMaxWidth = Math.max(weightedMaxWidth,
matchWidthLocally ? margin : measuredWidth);
} else {
alternativeMaxWidth = Math.max(alternativeMaxWidth,
matchWidthLocally ? margin : measuredWidth);
}
i += getChildrenSkipCount(child, i);
}
if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
mTotalLength += mDividerHeight;
}
if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
maxWidth = alternativeMaxWidth;
}
...
maxWidth += mPaddingLeft + mPaddingRight;
// Check against our minimum width
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
if (matchWidth) {
forceUniformWidth(count, heightMeasureSpec);
}
}
複製程式碼
這個方法非常長,系統會遍歷每個子元素,並且呼叫子元素的measureChildBeforeLayout方法:
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
複製程式碼
這個方法內部又在執行measure子元素的操作,當子元素全部測量完畢後,Linearlayout才會去測量自己的大小。
注意的點 : 測量的過程是從父類開始分發,遞迴的測量子元素,最後再測量父類。layout的過程是恰好相反的,我們後面再講。
4.3、關於measure可能會遇到的坑
View的measure一般是很複雜的,某些情況下得多次測量,所以為了保險起見,我們應該在layout結束後再去獲取View的寬和高。在實際需求中,比如,在activity中,你怎麼獲取某個view的width和height呢?有的人肯定會說,很簡答啊,直接在oncreat中去呼叫getWidth和getHeight,這肯定是不行的,你們可以去試試,這裡獲取到的極有可能是空值,這是因為View的繪製和Activity生命週期不存在同步的關係,無法保證在哪一個週期View的測量工作已經完成了,所以不靠譜。這裡簡單提一下幾種常見的解決方案,但是不展開講解了:
- Activity的onWindowsFocusChanged,在這個裡面去獲取寬和高。
- view.post(runnable),等到訊息佇列開始執行的時候,view肯定是ready狀態了。
- ViewTreeObserve 重寫addOnGlobalLayoutListener方法。
5、總結
1.對於measure過程,我們只要瞭解view和ViewGropup的大致流程就可以了,尤其注意 在自定義view 的時候要重寫onMeasure方法,並且給wrap_content佈局賦預設值。
2.獲取某個view寬和高的時機,這個很重要,在面試中經常被考察到。