androidView的繪製流程整理和記錄

view是我們作爲android開發,平時最常接觸的東西了。但是view的繪製流程和實現原理大多數人都是不知其所以然。所以我想整理一下View的繪製流程,加深印象,提升自己

View繪製的流程框架

所有View的繪製都是從ViewRoot的performTraversals方法開始的。ViewRoot對應於ViewRootImpl類,它是連接WindowManager和DecorView的紐帶。
如圖所示,performTraversals會依次調用performMeasure、performLayout、performDraw三個方法,這3個方法分別完成頂級View的measure、layout、draw這3個流程。其中performMeasure會調用measure方法,measure方法中又會調用onMeasure方法,這樣層層調用,最終遍歷整個View樹。

image

View的繪製是從上往下一層層迭代下來的。DecorView–> ViewGroup(—>ViewGroup)–> View,按照這個流程從上往下,依次measure(測量),layout(佈局),draw(繪製)。

image

Measure流程

View的measure流程是通過調用View的measure()方法進行的,measure方法又在內部調用了onMeasure()方法。因爲measure方法是一個final方法,子類無法重寫,我們只需要關注onMeasure()方法即可。

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

onMeasure() 中調用setMeasuredDimension()設定View的寬高信息,完成View的測量操作。其中參數widthMeasureSpec,heightMeasureSpec表示View的寬高的一些信息。

MeasureSpec的確定

先介紹下什麼是MeasureSpec?

image
MeasureSpec由兩部分組成,一部分是測量模式,另一部分是測量的尺寸大小。用這種格式主要是爲了避免更多的內存分配。

其中,Mode模式共分爲三類
UNSPECIFIED :不對View進行任何限制,要多大給多大,一般用於系統內部

EXACTLY:對應LayoutParams中的match_parent和具體數值這兩種模式。檢測到View所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值,

AT_MOST :對應LayoutParams中的wrap_content。View的大小不能大於父容器的大小。

那麼widthMeasureSpec和heightMeasureSpec這兩個參數是怎麼獲得的呢?

對於DecorView,其確定是通過屏幕的大小,和自身的佈局參數LayoutParams。
這部分很簡單,根據LayoutParams的佈局格式(match_parent,wrap_content或指定大小),將自身大小,和屏幕大小相比,設置一個不超過屏幕大小的寬高,以及對應模式。

對於其他View(包括ViewGroup),其確定是通過父佈局的MeasureSpec和自身的佈局參數LayoutParams。
這部分比較複雜。以下列圖表表示不同的情況:

image

View的測量流程如下:
image

Layout流程

知道了View的大小,就可以將View佈局在window中,view的佈局主要通過上下左右四個點來確定。

和measure過程不同的是ViewGroup現在layout()方法中確定自己的佈局,然後在onLayout()中再調用子View的layout()方法。而在Measure過程中,ViewGroup一般是先測量子View的大小,然後再確認自身的大小。

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);      
  ...

}  

上面看出通過 setFrame() / setOpticalFrame():確定View自身的位置,通過onLayout()確定子View的佈局。
setOpticalFrame()內部也是調用了setFrame(),所以具體看setFrame()怎麼確定自身的位置佈局。

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);
}

確定了自身的位置後,就要通過onLayout()確定子View的佈局。onLayout()是一個可繼承的空方法。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

如果當前View就是一個單一的View,沒有子View,就不需要實現該方法。

如果當前View是一個ViewGroup,就需要實現onLayout方法,該方法的實現個自定義ViewGroup時其特性有關,必須自己實現。

由此便完成了一層層的的佈局工作。

View的佈局流程:

image

Draw流程

View的繪製過程遵循如下幾步:
①繪製背景 background.draw(canvas)

②繪製自己(onDraw)

③繪製Children(dispatchDraw)

④繪製裝飾(onDrawScrollBars)

從源碼中可以清楚地看出繪製的順序。

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;
    }
    ...    
}

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

總結

通過繪製流程的分析,可以看出,androidView的繪製是符合直覺的。我們畫畫的時候是不是現在腦子裏想好要畫多大,再決定從哪裏開始繪製,最後拿起筆進行繪製呢。

如果要實現自定義View,根據需求的不同,我們可以自定義實現不同的方法來達到想要的效果。

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