View的工作流程主要是指measure、layout、draw這三大流程,即測量、佈局和繪製。其中measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置,而draw則將View繪製到屏幕上。
1、measure過程
measure過程要分情況來看,如果只是一個原始的View,那麼通過measure方法就可以完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子元素的measure方法,各個子元素再遞歸去執行這個流程,下面針對這兩種情況分別討論。
a.View的measure過程
View 的measure過程由其measure方法來完成,measure方法是一個final類型的方法,這就意味着子類不能重寫此方法,在View的measure方法中會去調用View的onMeasure方法,因此只需要看onMeasure的實現即可,View的onMeasure方法如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
//從下面的分析看,只有當前測量模式是系統內部的UNSPECTIFIED時,纔會調用getSuggestMinimumWidth方法。否則就是使用onMeasure形參裏面的Spec設置大小。
}
上面的代碼很簡介,但是簡潔不代表簡單,setMeasuredDimension會設置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://固定大小和match_parent就是這種模式
result = specSize;
break;
}
return result;
}
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
總結下:
從getDefaulSize方法的實現來看,View的寬/高由specSize決定(AT_MOST和EXACTLY模式下),所以我們可以得出如下結論:直接繼承View的自定義控件需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於使用match_parent。
爲什麼呢?這個原因需要結合上述代碼和之前的表才能更好地理解。從上述代碼中我們知道,如果View在佈局中使用wrap_content,那麼它的specMode是AT_MOST模式,在這種模式下,它的寬/高等於specSize;查表4-1可知,這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器當前剩餘的空間大小。很顯然,View的寬/高就等於父容器當前剩餘的空間大小,這種效果和在佈局中使用match_parent完全一致。如何解決這個問題呢?也很簡單,代碼如下所示。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);//寬高都設置成wrap_content,就重設下,避免使用父容器的寬高,導致和match_parent效果一樣。
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);//寬設置成wrap_content
} else if (eightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);//高設置成wrap_content,
}
}
我們翻下TextView的onMeasure實現看到是對AT_MOST模式做了處理的。
b.ViewGroup的measure過程
對於ViewGroup來說,除了完成自己的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時,會對每一個子元素進行測量,那麼這個方法就很好理解了
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方法(這個是重寫了View的onMeasure方法,我們每次測量子view的時候都會調到這裏,child.measure–>onMeasure)。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
上述的代碼很簡單我們選擇一個來看下,比如選中豎直方向的LinearLayout測量過程,即measureVertical,他的源碼還比較長,我們看:
// 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;
}
...
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
從上面的代碼可以看出,系統會遍歷子元素並對每一個子元素執行measureChildBeforeLayout方法,這個方法內部會調用子元素的measure方法,這樣各個子元素就開始依次進入measure過程,並且系統通過mTotalLength這個變量來存儲LinearLayout在豎直方向上的初步高度,每測量一個子元素,mTotalLength就會增加,增加的部分主要包括子元素的高度以及豎直方向上的margin等,當子元素測量完畢之後,LinearLayout會測量自己的大小,看源碼:
總結下:
- (1)Activity/View#onWindowFocusChanged。(會多次調用)
onWindowFocusChanged這個方法的含義是:View已經初始化完畢了,寬/高已經準備好了,這個時候去獲取寬/高是沒問題的。需要注意的是,onWindowFocusChanged會被調用多次,當Activity的窗口得到焦點和失去焦點時均會被調用一次。具體來說,當Activity繼續執行和暫停執行時,onWindowFocusChanged均會被調用,如果頻繁地進行onResume和onPause,那麼onWindowFocusChanged也會被頻繁地調用。代碼就是重寫這個方法,可自己寫下。
- (2)view.post(runnable)(推薦使用)
通過post可以將一個runnable投遞到消息隊列,然後等到Lopper調用runnable的時候,View也就初始化好了,典型代碼如下:
@Override
protected void onStart() {
super.onStart();
mTextView.post(new Runnable() {
@Override
public void run() {
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
}
- (3)ViewTreeObserver(回調方法可能多次調用)
使用ViewTreeObserver的衆多回調可以完成這個功能,比如使用OnGlobalLayoutListener這個接口,當View樹的狀態發生改變或者View樹內部的View的可見性發生改變,onGlobalLayout方法就會回調,因此這是獲取View的寬高一個很好的例子,需要注意的是,伴隨着View樹狀態的改變,這個方法也會被調用多次。
- (4)view.measure(int widthMeasureSpec , int heightMeasureSpec)(手動測量,個人覺得沒必要)
通過手動測量View的寬高,這種方法比較複雜,這裏要分情況來處理,根據View的LayoutParams來處理
match_parent:
直接放棄,無法測量出具體的寬高,根據View的測量過程,構造這種measureSpec需要知道parentSize,即父容器的剩下空間,而這個時候我們無法知道parentSize的大小,所以理論上我們不可能測量出View的大小
具體的數值,比如寬高都是100dp,那我們可以這樣:
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);//確定模式
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);
warap_content
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
mTextView.measure(widthMeasureSpec,heightMeasureSpec);
2、layout過程
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;
}
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
LinearLayout中onLayout的實現邏輯和onMeasure的實現邏輯類似,這裏選擇layoutVertical繼續講解,爲了更好地理解其邏輯,這裏給出了主要邏輯代碼:
void layoutVertical(int left, int top, int right, int bottom) {
...
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
...
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
我們注意到setChildFrame中的width和height實際上就是子元素測量寬高,從下面的代碼可以看出
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.max(0, childHeight), MeasureSpec.EXACTLY);
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,lp.width);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
而在Layout方法中通過setFrame去設置子元素的四個頂點位置,方法中有這麼幾句:
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
從getWidth和getHeight的源碼再結合mLeft、mTop、mRight、mBottom這四個變量的賦值過程來看,getWidth的返回值剛好就是View測量的寬度,而getHeight方法的返回值也剛好就是View的測量高度。
所以我們可以回答這個問題了:在View的默認實現中,View的測量寬高和最終寬高是一樣的,只不過一個是measure過程,一個是layout過程,而最終形成的是layout過程,即兩者的賦值時機不同,測量寬高的賦值時機,稍微早一些。因此,在日常開發中,我們可用認爲他們是相等的,但是還是有些不相同的。
舉例說明: 如果重寫View的layout方法,代碼如下:
public void layout(int l,int t,int r, int b){
super.layout(l,t,t+100,b+100);//人爲強制加了100px,導致4個座標和測量的不一樣。
}
3、draw過程
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
drawBackground(canvas);
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
...
}
View的繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷調用所有子元素的draw方法,如此draw事件就一層層地傳遞了下去。
View有個特殊的方法setWillNotDraw,先看下它的源碼,如下所示。
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}