在開發過程中,我們常常會來自定義View。它是用戶交互組件的基本組成部分,負責展示圖像和處理事件,通常被當做自定義組件的基類繼承。那麼今天就通過源碼來仔細分析一下View是如何被創建以及在繪制過程中發生了什麼。
創建
首先,View公有的構造函數的重載形式就有四種:
View(Context context) 通過代碼創建view時使用此構造函數,通過context參數,可以獲取到需要的主題,資源等等。
View(Context context, AttributeSet attrs) 當通過xml布局文件創建view時會使用此構造函數,調用了3個參數的構造方法。
View(Context context, AttributeSet attrs, int defStyleAttr) 通過xml布局文件創建view,並采用在屬性中指定的style。這個view的構造函數允許其子類在創建時使用自己的style。調用了下面四參的構造方法。
View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) 該構造函數可以通過xml布局文件創建view,可以采用theme屬性或者style資源文件指定的style。
參數:
Context : view運行的上下文信息,從中可以獲取到當前theme,資源文件等信息。
AttributeSet: xml布局文件中view標簽下指定的屬性集合。
defStyleAttr: 當前theme中的一條屬性,它包含一條指向theme資源文件中style的引用。默認值為0。
defStyleRes: 一個style資源文件的標示,表示style的ID,當值為0或者找不到對應的theme資源時候采用默認值。
綜上所述,單參的構造函數從代碼創建view,其余都調用四參的構造函數根據xml布局文件創建view。我們可以在不同的地方指定屬性值,例如:
直接在xml標簽中中指定的attrs值,可以從AttributeSet中獲取。
通過在標簽屬性“style”中指定的資源文件。
默認的defStyleAttr。
默認的defStyleRes。
當前theme中的默認值。
構造函數的代碼過長,就不在這裡貼了,主要進行的工作是:獲取各項系統定義的屬性,然後根據屬性值初始化view的各項成員變量和事件。
一般情況下,我們自定義view的時候,根據實際情況重寫構造函數時,如果只從code創建,則只用實現單參數的即可。如果需要從xml布局文件中創建,則需要實現單參數和一個多參數的就好了,因為多參數的默認調用了四參數的構造函數;然後再獲取到自定義的屬性進行處理就OK了。
至此,view的創建以及初始化工作完畢,然後開始繪制view的工作。那麼Android系統是如何對view進行繪制的呢?
繪制
在activity獲取到焦點後,會請求Android Framework根據它的布局文件進行繪制,activity需要提供所繪布局文件的根節點,然後對布局的樹結構一邊遍歷一邊進行繪制。我們都知道,ViewGroup是View的子類,它可以擁有若干子view,它的很多操作和view相同,不同的是ViewGroup負責繪制其子節點,而view則負責繪制其自身。整個遍歷過程從上到下,在整個過程中,需要進行大小測量(measure函數)和定位(layout函數),然後再進行繪制。下面我們來看這些工作是如何進行的:
測定尺寸
在Android中,所有view被組織成樹狀結構,最頂層measure的主要工作就是負責遞歸測量出整個view樹結構的尺寸大小,每個View的控件的實際寬高都是由父視圖和本身視圖決定的。
在研究源碼之前,我先從整體上概況一下整個遞歸調用過程。從根view開始,使用measure方法中計算整個view樹的大小,在該方法中調用子view的onMeasure方法。在onMeasure中主要進行兩個工作:
調用setMeasuredDimension設置view自身的尺寸(mMeasureWidth和mMeasuredHeight),具體會在下面看到。
如果該view是ViewGroup,則需要繼續遞歸調用其onMeasure方法來計算ViewGroup的子view大小。
根view通常就是一個ViewGroup,需要計算子view尺寸。首先獲取到所有子view,然後調用measureChildWithMargins方法來計算子view的尺寸。在這個方法中調用了子view的measure方法。下面我們來看具體源碼。
首先在measure方法中確定view的大小。這個方法被定義為final類型,不可被子類重寫。在View中有一個靜態內部類MeasureSpec封裝了父view要傳遞給子View的布局參數,由size 和 mode共同組成。size即是大小,mode表示模式。(其實就是一個int值高2位表示mode,低30位表示size). mode總共有三種模式:
UNSPECIFIED:父view並未指定子view的大小,可隨意根據開發人員需求指定view大小。
EXACTLY: 父view嚴格指定了子view的大小
AT_MOST: 子view的大小不超過該值
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
boolean optical = isLayoutModeOptical(this);//是否使用視覺邊界布局
if (optical != isLayoutModeOptical(mParent)) {// 當view和它的父viewGroup就是否采用視覺邊界布局不一致時
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
方法接收的兩個參數widthMeasureSpec和heightMeasureSpec表示view的寬高,由上一層父view計算後傳遞過來。view大小的測量工作在標紅的onMeasure方法中進行。我們在自定義view時往往需要重寫該方法,根據傳入的view大小以及其內容來設定view最終顯示的尺寸。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
重寫該方法時,我們需要調用setMeasuredDimension這個方法來存儲已經測量好的尺寸(這裡默認使用getDefalutSize),只有在調用過此方法後,才能通過getMeasuredWidth方法和getMeasuredHeight方法獲取到尺寸。同時,我們要保證最後得到的尺寸不小於view的最小尺寸。我們需要注意的是,setMeasuredDimension方法必須在OnMeasure方法中調用,否則會拋出異常。
OK,measure方法至此完畢。然而,我們可以發現真正測量view大小的工作並不在此方法中進行,這裡僅僅是一個測量框架,根據各種不同的情況進行判斷,完成一些必要的步驟。這些步驟是必須的也是無法被開發者更改的,需要根據情況自定義的工作放在了onMeasure中由開發者完成。這樣既保證了繪制流程的執行,又靈活的滿足了各種需求,是典型的模板方法模式。
由於一個父view下可能有多個子view,所以measure方法不僅僅執行一次,而是在父view(viewGroup)中獲取到所有子view,然後遍歷調用子view的measure方法。
定位
當view的大小已經設定完畢,則需要確定view在其父view中的位置,也就是把子view放在合理的位置上。因為只有ViewGroup才包含子view,所以一般我們說起父view,肯定是在說ViewGroup。完成布局工作主要分為兩部分,也是遞歸實���的:
1.在layout方法中調用setFrame設置該View視圖位於父視圖的坐標。
2.如果view是ViewGroup類型,則調用其onLayout方法完成子view布局工作。
下面來看具體源碼,父view調用了子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); // 確定view在布局中的位置
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;
}
該方法接收四個參數是子view相對於父view而言的上下左右位置。然而我們發現其中調用到的onLayout方法默認的實現是空的。這是因為確定view在布局的位置這個操作應該由Layout根據自身特點來完成。任何布局的定義都要重寫其onLayout方法,並在其中設定子view的位置。
繪制
在進行完測定尺寸和定位之後,終於可以開始繪制了。這裡的工作仍是通過遞歸來完成的。view調用draw方法來進行繪制,裡面調用onDraw來繪制自身,如果還有子view則需要調用dispatchDraw來繪制子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;
// Step 1, 繪制背景
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 如果不需要,跳過步驟2和5
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, 繪制內容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, 繪制子view
dispatchDraw(canvas);
// Step 6, 繪制裝飾部分
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 完成
return;
}
}
我們選擇常規的繪制過程,不介紹2,5步驟。
第一步,調用drawBackground繪制背景圖案:
private void drawBackground(Canvas canvas) {
final Drawable background = mBackground;
// 獲取到當前view的背景,是一個drawable對象 if (background == null) {
return;
}
if (mBackgroundSizeChanged) {// 判斷背景大小是否變化,是則設置背景邊界
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;
}
// Attempt to use a display list if requested.
if (canvas.isHardwareAccelerated() && mAttachInfo != null
&& mAttachInfo.mHardwareRenderer != null) {
mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);
final RenderNode displayList = mBackgroundRenderNode;
if (displayList != null && displayList.isValid()) {
setBackgroundDisplayListProperties(displayList);
((HardwareCanvas) canvas).drawRenderNode(displayList);
return;
}
}
// 調用drawable對象的繪制方法完成繪制
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
第三步,調用onDraw方法繪制view的內容,由於不同的view內容不同,所以需要子類進行重寫。
第四步,繪制子view,這裡仍然需要當前layout的dispatchDraw方法來完成對各子view的繪制。
第六步,繪制滾動條。
通常情況下,我們自定義view,復寫onDraw方法來繪制我們定義的view的內容即可。
總結
通過研究view類的源碼,我們可以發現,在整個view的繪制流程中我們需要完成測定尺寸,布局定位,繪制這三個步驟。Android在設計過程中,將固定不變的流程設計為不可更改的模板方法,然而需要根據不同情況而定的內容則交給開發者來完成重寫,在模板方法中調用即可。這樣設計即保證了整個流程的完整,又給開發工作帶來了靈活。同時,在類中又根據不同情況定義了不同的flag,來滿足不同情況的繪制需求,以後有機會再具體研究這些flag的具體意義。
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11