前言
本文大量參照《Android 開發(fā)藝術探索》及參考資料的內(nèi)容整合,主要幫助自己理清 View 的工作原理衅金。深入學習希望大家更多的關注參考資料耙考。
上一篇文章了解了 MeasureSpec 的概念及獲取耘斩,從名字上看就能了解到這是用來輔助測量過程的對象合陵,本次文章再來完整學習 View 的工作流程枢赔。
View 的工作流程主要指 measure、layout拥知、draw 這三個過程:
- measure:確定 View 的測量寬/高
- layout:確定 View 的最終寬/高和四個頂點位置
- draw:將 View 繪制到屏幕上
measure
如果是一個原始的 View踏拜,那么通過 measure 方法就完成了其測量過程。
如果是一個 ViewGroup低剔,那么除了完成自己的測量過程之外速梗,還會遍歷去調(diào)用所有子元素的 measure()肮塞。
View 的 measure 過程
View 的 measure 過程由其 measure 方法完成,在 measure() 中會調(diào)用 onMeasure() 方法姻锁,這是實際測量的地方枕赵,代碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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:
result = specSize;
break;
}
return result;
}
可以看到邏輯十分簡單位隶,當 measureSpec 的模式為 AT_MOST 或 EXACTLY 時烁设,specSize 即為 View 測量后的大小。
而當為 UNSPECIFIED钓试,View 測量大小為 getSuggestedMinimumWidth() 的值,會根據(jù) View 的 android:minWidth 和 background 屬性獲取一個值副瀑,由于這種情況一般用于系統(tǒng)內(nèi)部弓熏,所以就不深究,源碼十分簡單糠睡,大家可以親自動手一試挽鞠。
另外這里可以留意到,當你的 View 的寬高設置為 wrap_content 時狈孔。該 View 的 specSize 為 parentSize信认,specMode 為 AT_MOST;結果根據(jù)上面代碼 View 的測量寬高就是父容器的寬高均抽,這樣的結果跟 match_parent 就沒有區(qū)別嫁赏!
所以實際上像 TextView 這些控件,都會重寫 onMeasure 方法油挥,根據(jù) View 的 LayoutParams 為 wrap_content 的情況設置一個寬高潦蝇,通常是小于父容器的。
ViewGroup 的 measure 過程
ViewGroup 是一個繼承自 View 的抽象類深寥,它并沒有重寫 View 的 onMeasure 方法攘乒,而是由其抽象實現(xiàn)類例如 LinearLayout 來重寫,因為不同的 ViewGroup 有不同的特性惋鹅。
ViewGroup 提供了一個 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);
}
}
}
就是調(diào)用 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);
}
這里 getChildMeasureSpec 方法就十分熟悉了,根據(jù)子元素的 LayoutParams 和 父容器的 measureSpec 創(chuàng)建子元素的 measureSpec闰集,然后調(diào)用子元素的 measure 方法沽讹,這樣就是上一節(jié)的內(nèi)容了。
如此遞歸完成了 View 的測量過程返十。
layout
layout 的作用是確定元素的位置妥泉,當 ViewGroup 調(diào)用 layout 方法確定自己的位置后,又會調(diào)用 onLayout 來確定所有子元素的位置洞坑。
View 的 layout 過程
實際上 ViewGroup.layout() 中會通過 super.layout 調(diào)用其父類盲链,即 View 的 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;
}
一些判斷和代碼細節(jié)可以略過,這里主要有兩句本慕,一是調(diào)用 setFrame 方法確定了 View 的四個頂點位置锅尘,接著調(diào)用 onLayout 方法,該方法負責父容器確定子元素的位置。所以 View 和 ViewGroup 的 onLayout 都沒有進行具體的實現(xiàn),因為不同的布局有不同的特性骨杂,我們下面來看一下 LinearLayout 的實現(xiàn)。
LinearLayout 的 onLayout 過程
直接上代碼~
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);
}
}
這個 mOrientation 很明顯了陕凹,我們只看垂直方向的 layoutVertical 方法拂盯,代碼比較長:
void layoutVertical(int left, int top, int right, int bottom) {
...
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);
}
}
}
去除了一些細節(jié)內(nèi)容空凸,LinearLayout 的 layoutVertical 會調(diào)用 setChildFrame 來為子元素指定對應的位置紊选;同時變量 childTop 會不斷增加,使得后面的元素位置逐漸靠下巩那,跟我們平時使用 LinearLayout 的效果特性是符合的裆赵。
setChildFrame 方法的代碼如下:
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
調(diào)用了子元素的 layout 方法,這樣又是逐步遞歸調(diào)用陈醒,完成 View 的布局流程钉跷。
draw
Draw 的作用是把 View 繪制到屏幕上來朦促。繪制時的流程圖如下:
具體再來看看 draw 方法的代碼:
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
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;
if (!dirtyOpaque) {
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
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(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);
// we're done...
return;
}
...
}
其中逐步執(zhí)行了流程圖中的內(nèi)容箩退,并且在不需要時會跳過 layer 的繪制以提高效率离熏。
在 View 中,onDraw() 和 dispatchDraw() 都為空實現(xiàn)戴涝,因為不同的控件會有不同的繪制方式滋戳。于是我們也意識到钻蔑,自定義繪制過程需要復寫 onDraw 方法來繪制自身的內(nèi)容。
而 ViewGroup 對 dispatchDraw() 則進行了實現(xiàn)胧瓜,在其中通過 drawChild() 調(diào)用了子元素的 draw 方法矢棚,又是遞歸完成了繪制過程。
這里涉及到動畫等內(nèi)容的繪制府喳,比較復雜蒲肋,與理解 View 的工作原理的關系不強,所以可能在以后學習一些簡單的自定義控件時再來學習控件是通過何種方式進行繪制的钝满。
以上便是 View 的工作流程兜粘,再次聲明,大量參考學習了他人的文章內(nèi)容弯蚜。
希望能對大家有所幫助孔轴,感謝這些優(yōu)秀文章的作者們。