1. 程式人生 > >View繪圖原理總結

View繪圖原理總結

基本操作由三個函式完成:measure()、layout()、draw(),其內部又分別包含了onMeasure()、onLayout()、onDraw()三個子方法(回撥函式)。

整個View樹的繪圖流程是在ViewRoot.java類的performTraversals()函式展開的,該函式做的執行過程可簡單概況為根據之前設定的狀態,判斷是否需要重新計算檢視大小(measure)、是否重新需要安置檢視的位置(layout)、以及是否需要重繪(draw)。

View繪製流程圖

其中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。