View的工作流程
前面《Android View的工作原理(一)之 View的三大過程和 認識MeasureSpec》中我們介紹了View出現在螢幕要經過三個過程:measure、layout 和 draw和它們各自的作用,以及MeasureSpec的獲得情況。今天将沿着前面内容繼續探讨View的工作原理,其中大家就會明白MeasureSpec的用途和三大過程的工作流程。
measure過程
measure就是測量,在某些極端情況下,系統可能需多次measure才能确定最終的測量寬/高,在這情形下,在onMeasure方法中拿到的測量寬/高很可能是不準确的。一個比較好的習慣是在onLayout方法中去擷取View的測量寬/高或最終寬/高。
View的measure過程
View是通過measure方法就完成了其測量過程。View的measure是一個 final方法。在measure方法中會去調用onMeasure方法,onMeasure方法如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
this.setMeasuredDimension(getDefaultSize (this.getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize (this.getSuggestedMinimumHeight(), heightMeasureSpec));
}
setMeasuredDimension方法會設定View寬/高的測量值,再來看看getDefaultSize方法代碼:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
switch(specMode) {
case MeasureSpec.EXACTLY:
case MeasureSpec.AT_MOST:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
result = size;
break;
}
return result;
}
在AT_MOST和EXACTLY兩種情況下,傳回的大小其實就是前面提到的MeasureSpec中的SpecSize,而這個specSize就是View測量後的大小(最終大小是在layout階段确定)。
在UNSPECIFIED情況下,一般用于系統内部的測量過程,傳回大小為第一個參數size,即getSuggestedMinimumWidth和getSuggestedMinimumHeight兩個方法傳回值,源碼如下:
protected int getSuggestedMinimumWidth() {
int suggestedMinWidth = this.mMinWidth;
if(this.mBGDrawable != null) {
int bgMinWidth = this.mBGDrawable.getMinimumWidth();
if(suggestedMinWidth < bgMinWidth) {
suggestedMinWidth = bgMinWidth;
}
}
return suggestedMinWidth;
}
getSuggestedMinimumWidth的邏輯:如果View沒有設定背景,那麼傳回mMinWidth的值(android:minWidth屬性的值,不設定則為0);如果設定了背景,則傳回mMinWidth和背景的最小寬度兩者中最大值。
ViewGroup的measure過程
ViewGroup除了要完成自己的measure過程外,還要周遊所有子元素的measure方法讓所有的子元素都執行measure過程。和View不同的是,ViewGroup是一個抽象類,是以它沒有重寫View的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);
}
}
}
ViewGroup在measure時,會對每一個子元素進行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);
}
LinearLayout的onMeasure方法分析
ViewGroup并沒有定義其measure的具體過程,這是因為它是一個抽象類,其measure過程的onMeasure方法需要各個子類去具體實作,比如LinearLayout、RelativeLayout等,我們來看看LinearLayout的onMeasure方法,如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
可以看到LinearLayout中垂直和水準方式分别由measureVertical和measureHorizontal兩個方法來具體實作。
layout過程
layout就是布局,layout的作用是ViewGroup用來确定子元素的位置,當ViewGroup的位置被确定後,它在onLayout中會周遊所有的子元素并調用期layout方法,在layout方法中onLayout方法會被調用。來看View的layout方法,如下所示:
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
首先會通過setFrame方法來設定View的四個頂點位置,即初始化mLeft、mTop、mBottom和mRight;
接下來調用onLayout方法來确定子元素的位置,onLayout的具體實作同樣和具體的布局有關,是以View和ViewGroup均沒有真正實作onLayout方法,都交由繼續它們的子類各自實作。
draw過程
draw就是最後的繪制了,draw過程比較簡單,它的作用是将View繪制到螢幕上面。繪制過程遵循如上幾步:
1、 繪制背景
2、 繪制自己(onDraw方法)
3、 繪制子元素(dispatchDraw方法)
4、 繪制裝飾(onDrawScrollBars方法)
draw的源碼這裡略過,我們來看看View有一個特殊的方法setWillNotDraw,先看一下它的源碼:
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
如果一個View不需要繪制任何内容,那麼會設定一個标記為true,系統會進行相應的優化。預設情況下,ViewGroup就是啟用了這個優化标記,是以ViewGroup預設是不進行繪制功能的。當我們繼承ViewGroup自定義一個ViewGroup控件而且需要ViewGroup本身進行繪制的情況下,那麼就需要使用setWillNotDraw方法傳遞false。
——本博文部分内容參考自《Android開發藝術探索》