View繪圖原理總結
基本操作由三個函式完成:measure()、layout()、draw(),其內部又分別包含了onMeasure()、onLayout()、onDraw()三個子方法(回撥函式)。
整個View樹的繪圖流程是在ViewRoot.java類的performTraversals()函式展開的,該函式做的執行過程可簡單概況為根據之前設定的狀態,判斷是否需要重新計算檢視大小(measure)、是否重新需要安置檢視的位置(layout)、以及是否需要重繪(draw)。
其中measure、layout、draw可以看做動詞,measure(測量大小)後才能進行layout(確定位置),然後才能draw(繪製),他們都是public的方法,前兩個還是final的,根本沒打算被子類繼承。真正有變數的是on...回撥函式,繼承View(ViewGroup)時可以Override,on可以理解為當...時。
具體流程如下:
1、measure
用於計算View Tree的大小,即檢視的寬度和長度,回撥onMeasure函式。
// View.java中measure的函式原型 public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~MEASURED_DIMENSION_SET; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); } // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
onMeasure函式需要兩個引數:widthMeasureSpec和heightMeasureSpec(width and height measure specifications),這是兩個int值,包含兩部分:mode和size(高兩位表示mode,低30位表示size,具體參見MeasureSpec類),在onMeasure計算出的widht和height應該儘量滿足measure specifications,否則父容器可以選擇如clipping,scrolling或者丟擲異常,或者(也許是用新的specifications)再次呼叫onMeasure()。
一但width和height計算好了,就應呼叫View.setMeasuredDimension(int measuredWidth, int measuredHeight)方法對View的成員變數mMeasuredWidth和mMeasuredHeight變數賦值,否則將導致丟擲異常,而measure的主要目的就是對View樹中的每個View的mMeasuredWidth和mMeasuredHeight進行賦值,一旦這兩個變數被賦值,則意味著該View的測量工作結束。
// onMeasure的函式原型
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
// setMeasuredDimension的函式原型
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= MEASURED_DIMENSION_SET;
}
View預設的onMeasure函式實現就只調用了一個setMeasureDimension函式,過載的話(ViewGroup必須過載實現onMeasure函式)大致流程應該如下所示:
// 過載的onMeasure過程
private void onMeasure(int widthMeasureSpec , int heightMeasureSpec) {
// 設定該view的實際寬(mMeasuredWidth)高(mMeasuredHeight)
// 1、該方法必須在onMeasure呼叫,否者報異常。
setMeasuredDimension(w, h)
// 2、如果該View是ViewGroup型別,則對它的每個子View進行measure()過程
int childCount = getChildCount()
for(int i=0; i < childCount; i++) {
// 2.1、獲得每個子View物件引用
View child = getChildAt(i)
// 整個measure()過程就是個遞迴過程
// 該方法只是一個過濾器,最後會呼叫measure()過程 ;或者 measureChild(child , h, i)方法
measureChildWithMargins(child, h, i)
// 其實,對於我們自己寫的應用來說,最簡單的辦法是去掉框架裡的該方法,直接呼叫view.measure(),如下:
// child.measure(h, l)
}
}
// measureChildWithMargins具體實現在ViewGroup.java裡。
protected void measureChildWithMargins(View v, int height, int width) {
v.measure(h, l)
}
在自定義的View中需要過載onMeasure函式的話,關於MeasureSpec可能需要如下程式碼:
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else { // MeasureSpec.UPSPECIFIED
// Measure the text
result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
+ getPaddingRight();
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
2、layout
用於設定檢視在螢幕中顯示的位置,有兩個基本操作(1)setFrame(l,t,r,b),l,t,r,b即子檢視在父檢視中的具體位置,該函式用於將這些引數儲存起來;(2)onLayout(),在View中這個函式什麼都不會做,提供該函式主要是為viewGroup型別佈局子檢視用的;
layout函式原型,位於View.java
/* final 識別符號 , 不能被過載 , 引數為每個檢視位於父檢視的座標軸
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b); //設定每個檢視位於父檢視的座標軸
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);//回撥onLayout函式 ,設定每個子檢視的佈局
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
}
如果該View是個ViewGroup型別,需要遍歷每個子檢視childView,呼叫該子檢視的layout()方法去設定它的座標值。
//回撥View視圖裡的onLayout過程 ,該方法只由ViewGroup型別實現
private void onLayout(int left, int top, right, bottom) {
//如果該View不是ViewGroup型別
//呼叫setFrame()方法設定該控制元件的在父檢視上的座標軸
setFrame(l ,t , r ,b) ;
//--------------------------
//如果該View是ViewGroup型別,則對它的每個子View進行layout()過程
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
//2.1、獲得每個子View物件引用
View child = getChildAt(i) ;
//整個layout()過程就是個遞迴過程
child.layout(l, t, r, b) ;
}
}
3、draw
利用前兩部得到的引數,將檢視顯示在螢幕上,到這裡也就完成了整個的檢視繪製工作。值得注意的是每次發起繪圖時,並不會重新繪製每個View樹的檢視,而只會重新繪製那些"需要重繪”的檢視,View類內部變數包含了一個標誌位DRAWN,當該檢視需要重繪時,就會為該View新增該標誌位。有三個基本操作(1)繪製背景;(2)如果要檢視顯示漸變框,這裡會做一些準備工作;(3)繪製檢視本身,即呼叫onDraw()函式。在view中onDraw()是個空函式,每個View都需要過載該方法。而ViewGroup則不需要實現該函式,因為作為容器是“沒有內容“的,其包含了多個子view,而子View已經實現了自己的繪製方法,因此只需要告訴子view繪製自己就可以了,也就是下面的dispatchDraw()方法。(4)繪製子檢視,即dispatchDraw()函式。在view中不需要過載該方法,值得說明的是,ViewGroup類已經為我們重寫了dispatchDraw()的功能實現,應用程式一般不需要重寫該方法,但可以過載父類函式實現具體的功能。(5)如果需要(應用程式呼叫了setVerticalFadingEdge或者setHorizontalFadingEdge),開始繪製漸變框;(6)繪製滾動條;
//回撥View視圖裡的onLayout過程 ,該方法只由ViewGroup型別實現
private void draw(Canvas canvas){
//該方法會做如下事情
//1 、繪製該View的背景
//2、為繪製漸變框做一些準備操作
//3、呼叫onDraw()方法繪製檢視本身
//4、呼叫dispatchDraw()方法繪製每個子檢視,dispatchDraw()已經在Android框架中實現了,在ViewGroup方法中。
// 應用程式程式一般不需要重寫該方法,但可以捕獲該方法的發生,做一些特別的事情。
//5、繪製漸變框
}
//ViewGroup.java中的dispatchDraw()方法,應用程式一般不需要重寫該方法
@Override
protected void dispatchDraw(Canvas canvas) {
//其實現方法類似如下:
int childCount = getChildCount() ;
for(int i=0 ;i<childCount ;i++){
View child = getChildAt(i) ;
//呼叫drawChild完成
drawChild(child,canvas) ;
}
}
//ViewGroup.java中的dispatchDraw()方法,應用程式一般不需要重寫該方法
protected void drawChild(View child,Canvas canvas) {
// ....
//簡單的回撥View物件的draw()方法,遞迴就這麼產生了。
child.draw(canvas) ;
//.........
}
從上面可以看出自定義View需要最少覆寫onMeasure()和onDraw()兩個方法,自定義viewGroup的時候需要最少覆寫onMeasure()和onLayout()方法。
整個介面的更新依次執行measure、layout、draw操作,從ViewRoot按照從根到葉子的順序繪製view。