View的onMeasure()、onLayout()、onDraw()總結

自定義View是android開發中常有的一項工作,要想自定義View,你就必須熟練掌握View的測量、佈局及繪製,瞭解其原理並會使用。

View視圖繪製需要搞清楚兩個問題,一個是從哪裏開始繪製,一個是怎麼繪製?

先說從哪裏開始繪製的問題:我們平常在使用Activity的時候,都會調用setContentView來設置佈局文件,沒錯,視圖繪製就是從這個方法開始的;

再來說說怎麼繪製的:

在我們的Activity中調用了setContentView之後,會轉而執行PhoneWindow的setContentView,在這個方法裏面會判斷我們存放內容的ViewGroup(這個ViewGroup可以是DecorView也可以是DecorView的子View)是否存在。不存在的 話則會創建一個DecorView出來,並且會創建出相應的窗體風格,存在的話則會刪除原先ViewGroup上面已有的View,接着會調用 LayoutInflater的inflate方法以pull解析的方式將當前佈局文件中存在的View通過addView的方式添加到 ViewGroup上面來,接着在addView方法裏面就會執行我們常見的invalidate方法了,這個方法不只是在View視圖繪製的過程中經常 用到,其實動畫的實現原理也是不斷的調用這個方法來實現視圖不斷重繪的,執行這個方法的時候會調用他的父View的invalidateChild方法, 這個方法是屬於ViewParent的,ViewGroup以及ViewRootImpl中都對他進行了實現,invalidateChild裏 面主要做的事就是通過do while循環一層一層計算出當前View的四個點所對應的矩陣在ViewRoot中所對應的位置,那麼有了這個矩陣的位置之後最終都會執行到 ViewRootImpl的invalidateChildInParent方法,執行這個方法的時候首先會檢查當前線程是不是主線程,因爲我們要開始準 備更新UI了,不是主線程的話是不允許更新UI的,接着就會執行scheduleTraversals方法了,這個方法會通過handler來執行 doTraversal方法,在這個方法裏面就見到了我們平常所熟悉的View視圖繪製的起點方法performTraversals了;

一、總體流程

視圖的測量、佈局、繪製都是按照視圖樹從上到下的,大致可分爲DecorView-->ViewGroip-->View 這樣三個層級

當Activity對象被創建完成,會將DecorView添加到Window中(顯示),同時創建ViewRoot的實現對象ViewRootImpl與之關聯。ViewRootImpl會調用performTraversals來進行View的繪製過程。經過measure,layout,draw三個流程才能完成一個View的繪製過程,分別是用於測量寬、高;確定在父容器中的位置;繪製在屏幕上三個過程。而measure方法會調用onMeasure函數,這其中又會調用子元素的measure函數,如此反覆就能完成整個View樹的遍歷過程。其他兩個流程也同樣如此。

         measure決定了View的寬和高,測量之後就可以根據getMeasuredWidth和getMeasuredHeight來獲取View測量後的寬和高,幾乎等於最終的寬和高,但有例外;layout過程決定了View四個頂點的位置和實際的寬和高,完成之後可以根據getTop,getBottom,getLeft,getRight來獲得四個頂點的位置,並且可以使用getWidth和getHeight來獲取實際的寬和高;draw過程就決定了View的顯示,完成draw才能真正顯示出來。

二、onMeasure()

我們先來看看方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

裏面有兩個重要的參數 一個是widthMeasureSpec,一個是heightMeasureSpec,他們叫做MeasureSpec,它是系統將View的參數根據父容器的規則轉換而成的,之後根據它來測量出View的寬和高。它實際上是一個32位的int值,高二位表示SpecMode,就是測量的模式;低30位表示SpecSize,即在某種測量模式下的規格大小。

MeausureSpec有三種模式,常用的由兩種:EXACTLY和AT_MOST。EXACTLY表示父容器已經檢測出View所需要的精確大小(即父容器根據View的參數已經可以確定View的大小了),這時View的最終大小就是SpecSize的值,它對應於View參數中的match_parent和具體大小數值這兩種模式;AT_MOST表示父容器指定了一個可用大小的數值,記錄在SpecSize中,View的大小不能大於它,但具體的值還是看View的具體實現。它對應於View參數中的wrap_content。

DecorView的測量由窗口的大小和自身的LayoutParams決定,具體邏輯由getRootMeasureSpec決定,如果是具體值或者是match_parent,就是精確模式;如果是wrap_content就是最大模式;普通View的measure實際上是由父元素進行調用的(遍歷),父元素調用child的measure之前使用getChildMeasureSpec來轉換得到子元素的MeasureSpec(具體代碼:藝術探索P180-181),總結而來就是與自身的參數以及父元素的SpecMode有關:1、如果View的參數是具體值,那麼不管父元素的Mode是什麼,子元素的Mode都是精確模式並且大小就是參數的大小;2、如果View的參數是match_parent,如果父元素的mode是精確模式那麼View也是精確模式並且大小是父元素剩餘的大小;如果父元素的mode是最大模式,那麼View也是最大模式;3、如果View的參數是wrap_content,那麼View的模式一定是最大化模式,並且不能超過父容器的剩餘空間。看圖,清楚!

View自身的onMeasure方法就是把MeasureSpec的Size設爲最終的測量結果,這樣的測量問題就是match_parent和wrap_content是一樣的結果(因爲wrap_content的Size是最大可用Size),所以如果自定義View直接繼承自View,就需要對wrap_content進行處理,ImageView等都對wrap_content進行了特殊處理。

ViewGroup的measure過程:

         ViewGroup不同於View,它是一個抽象類,沒有實現onMeasure方法(因爲具體的Layout佈局特性各不相同),但它measure時會遍歷children調用measureChild,執行getChildMeasureSpec進行子元素的MeasureSpec創建,創建過程之前已經瞭解了,就是利用自身的Spec與子元素參數進行創建。

三、onLayout()

public void layout(int l, int t, int r, int b) {
    // 當前視圖的四個頂點
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    // setFrame( ) / setOpticalFrame( ) : 確定View自身的位置
    // 即初始化四個頂點的值, 然後判斷當前View大小和位置是否發生了變化並返回
    boolean changed = isLayoutModeOptical(mParent) ?
    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    //如果視圖的大小和位置發生變化, 會調用onLayout( )
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        // onLayout( ) : 確定該View所有的子View在父容器的位置
        onLayout(changed, l, t, r, b);
    }
    ...
}

protected boolean setFrame(int left, int top, int right, int bottom) {
    ...
    // 通過以下賦值語句記錄下了視圖的位置信息, 即確定View的四個頂點
    // 即確定了視圖的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}

layout的作用是ViewGroup來確定子元素的位置,當ViewGroup的位置被確定了之後,它就在自己的onLayout函數中遍歷所有的子元素並調用其layout方法,確定子元素的位置,對於子元素View,layout中又會調用其onLayout函數。View和ViewGroup中都沒有真正實現onLayout方法。但View和ViewGroup的layout方法是一致的,作用都是用於確定自己的位置,layout方法會調用setFrame方法來設定View的四個頂點的位置,即初始化mLeft,mRight,mTop,mBottom四個值,這樣就確定了View在父元素中的位置。

問題:getMeasuredHeight和getHeight有什麼區別(同Width)?

在measure之後就可以使用getMeasuredHeight來進行獲取測量的寬和高,而layout過程是晚於measure的,ViewGroup的setChildFrame會調用child的layout來確定child的真實位置,源代碼中也可以看出layout的bottom和top就是利用getMeasuredHeight和getMeasuredWidth來計算的,所以說如果child的layout不重寫,那麼就是一樣的!如果child的layout函數被重寫,就會有不一樣的結果。

—》getMeasuredHeight是控件的實際高度,與屏幕無關。

而onDraw則是在view繪畫的時候使用的。 getHeight既然是在繪畫的時候調用,那麼必然是顯示多少繪畫多少,所以這個高度會隨着view在屏幕的顯示情況來onDraw,所以getHeight是隨着view在屏幕的顯示而不同的。

—》getHeight得到的是view的顯示高度,跟view在屏幕的顯示情況有關。

四、onDraw()

public void draw(Canvas canvas) {
    // 所有的視圖最終都是調用 View 的 draw ( ) 繪製視圖( ViewGroup 沒有複寫此方法)
    // 在自定義View時, 不應該複寫該方法, 而是複寫 onDraw(Canvas) 方法進行繪製。
    // 如果自定義的視圖確實要複寫該方法, 那麼需要先調用 super.draw(canvas)完成系統的繪製, 然後再進行自定義的繪製。
    ...
    int saveCount;
    if (!dirtyOpaque) {
        // 步驟1: 繪製本身View背景
        drawBackground(canvas);
    }
    // 如果有必要, 就保存圖層( 還有一個復原圖層)
    // 優化技巧:
    // 當不需要繪製 Layer 時, “保存圖層“和“復原圖層“這兩步會跳過
    // 因此在繪製的時候, 節省 layer 可以提高繪製效率
    final int viewFlags = mViewFlags;
    if (!verticalEdges && !horizontalEdges) {
        if (!dirtyOpaque)
            // 步驟2: 繪製本身View內容 默認爲空實現, 自定義View時需要進行復寫
            onDraw(canvas);
        ......
        // 步驟3: 繪製子View 默認爲空實現 單一View中不需要實現, ViewGroup中已經實現該方法
        dispatchDraw(canvas);
        ........
        // 步驟4: 繪製滑動條和前景色等等
        onDrawScrollBars(canvas);
        ..........
        return;
     }
     .......

}

View的繪製會分爲四步:

①繪製背景 background.draw(canvas)
②繪製自己( onDraw)
③繪製Children(dispatchDraw)
④繪製裝飾( onDrawScrollBars)

無論是ViewGroup還是單一的View, 都需要實現這套流程, 不同的是, 在ViewGroup中, 實現了 dispatchDraw()方法, 而在單一子View中不需要實現該方法。 自定義View一般要重寫onDraw()方法, 在其中繪製不同的樣式。

OnDraw中進行繪製自己的操作。使用canvas進行繪製等等,簡單來說就是利用代碼畫自己需要的圖形。

繪製過程中的旋轉以及save和restore

Android中的旋轉rotate旋轉的是座標系,也就是說旋轉畫布實際上畫布本身的內容是不動的,不會直接把畫布上已經存在的內容進行移動,只是旋轉座標系,這樣旋轉之後的操作全部是針對這個新的座標系的。

save就是保存當前的的座標系,之後再調用restore時,座標系復原到save保存的狀態,這兩個函數restore的次數不能大save,否則會引發異常。

常用優化技巧:

1.onDraw()中不要創建新的局部對象

onDraw()會被頻繁調用,如果方法內部創建了局部對象,則會一瞬間產生大量的臨時對象,這使得佔用過多內存,系統頻繁GC,降低了程序執行效率。

2.避免onDraw()中執行大量耗時操作

View的最佳繪製頻率爲60fps,因爲LCD的頻率是60Hz,顯示每一幀的間隔是16ms,所以每一個VSync信號的時間間隔是16ms,接收到該信號時視圖會進行刷新,如果你繪畫時間過長就會導致View繪製不流暢,可以使用多線程來解決。

3.避免Overdraw

在同一個地方繪製多次肯定是浪費資源的,也避免浪費資源去渲染那些不必要和看不見的背景。你可以在手機的開發者設置中開啓起調試GPU過度繪製選項來查看視圖繪製的情況。

 

五、總結

從View的測量、 佈局和繪製原理來看, 要實現自定義View, 根據自定義View的種類不同, 可能分別要自定義實現不同的方法。 但是這些方法不外乎: onMeasure()方法, onLayout()方法, onDraw()方法。
onMeasure()方法: 單一View, 一般重寫此方法, 針對wrap_content情況, 規定View默認的大小值, 避免於match_parent情況一致。 ViewGroup, 若不重寫, 就會執行和單子View中相同邏輯, 不會測量子View。 一般會重寫onMeasure()方法, 循環測量子View。
onLayout()方法:單一View, 不需要實現該方法。 ViewGroup必須實現, 該方法是個抽象方法, 實現該方法, 來對子View進行佈局。View測量、 佈局及繪製原理
onDraw()方法: 無論單一View, 或者ViewGroup都需要實現該方法, 因其是個空方法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章