圖解View測量、布局及繪制原理

Android中自定義View一直是一個高級的技能,入門比較難,看起來很高大上。想要學(xué)會自定義View,當(dāng)然要理解View的測量、布局及繪制原理,本篇文章將以圖表的形式講解View的測量、布局及繪制原理。

一、View繪制的流程框架


View的繪制是從上往下一層層迭代下來的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照這個流程從上往下,依次measure(測量),layout(布局),draw(繪制)。關(guān)于DecorView,可以看這篇文章。

二、Measure流程

顧名思義,就是測量每個控件的大小。

調(diào)用measure()方法,進行一些邏輯處理,然后調(diào)用onMeasure()方法,在其中調(diào)用setMeasuredDimension()設(shè)定View的寬高信息,完成View的測量操作。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
}

measure()方法中,傳入了兩個參數(shù) widthMeasureSpec, heightMeasureSpec 表示View的寬高的一些信息。

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

由上述流程來看Measure流程很簡單,關(guān)鍵點是在于widthMeasureSpec, heightMeasureSpec這兩個參數(shù)信息怎么獲得?

如果有了widthMeasureSpec, heightMeasureSpec,通過一定的處理(可以重寫,自定義處理步驟),從中獲取View的寬/高,調(diào)用setMeasuredDimension()方法,指定View的寬高,完成測量工作。

MeasureSpec的確定

先介紹下什么是MeasureSpec?


MeasureSpec由兩部分組成,一部分是測量模式,另一部分是測量的尺寸大小。

其中,Mode模式共分為三類

UNSPECIFIED :不對View進行任何限制,要多大給多大,一般用于系統(tǒng)內(nèi)部

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

AT_MOST :對應(yīng)LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

那么MeasureSpec又是如何確定的?

對于DecorView,其確定是通過屏幕的大小,和自身的布局參數(shù)LayoutParams。

這部分很簡單,根據(jù)LayoutParams的布局格式(match_parent,wrap_content或指定大?。?,將自身大小,和屏幕大小相比,設(shè)置一個不超過屏幕大小的寬高,以及對應(yīng)模式。

對于其他View(包括ViewGroup),其確定是通過父布局的MeasureSpec和自身的布局參數(shù)LayoutParams。

這部分比較復(fù)雜。以下列圖表表示不同的情況:


當(dāng)子View的LayoutParams的布局格式是wrap_content,可以看到子View的大小是父View的剩余尺寸,和設(shè)置成match_parent時,子View的大小沒有區(qū)別。為了顯示區(qū)別,一般在自定義View時,需要重寫onMeasure方法,處理wrap_content時的情況,進行特別指定。

從這里看出MeasureSpec的指定也是從頂層布局開始一層層往下去,父布局影響子布局。

可能關(guān)于MeasureSpec如何確定View大小還有些模糊,篇幅有限,沒詳細(xì)具體展開介紹,可以看這篇文章

View的測量流程:

三、Layout流程

測量完View大小后,就需要將View布局在Window中,View的布局主要通過確定上下左右四個點來確定的。

其中布局也是自上而下,不同的是ViewGroup先在layout()中確定自己的布局,然后在onLayout()方法中再調(diào)用子View的layout()方法,讓子View布局。在Measure過程中,ViewGroup一般是先測量子View的大小,然后再確定自身的大小。

public void layout(int l, int t, int r, int b) {  

    // 當(dāng)前視圖的四個頂點
    int oldL = mLeft;  
    int oldT = mTop;  
    int oldB = mBottom;  
    int oldR = mRight;  

    // setFrame() / setOpticalFrame():確定View自身的位置
    // 即初始化四個頂點的值,然后判斷當(dāng)前View大小和位置是否發(fā)生了變化并返回  
 boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    //如果視圖的大小和位置發(fā)生變化,會調(diào)用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()內(nèi)部也是調(diào)用了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) {
    }

如果當(dāng)前View就是一個單一的View,那么沒有子View,就不需要實現(xiàn)該方法。

如果當(dāng)前View是一個ViewGroup,就需要實現(xiàn)onLayout方法,該方法的實現(xiàn)個自定義ViewGroup時其特性有關(guān),必須自己實現(xiàn)。

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

View的布局流程:

四、Draw過程

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

②繪制自己(onDraw)

③繪制Children(dispatchDraw)

④繪制裝飾(onDrawScrollBars)

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

public void draw(Canvas canvas) {
// 所有的視圖最終都是調(diào)用 View 的 draw ()繪制視圖( ViewGroup 沒有復(fù)寫此方法)
// 在自定義View時,不應(yīng)該復(fù)寫該方法,而是復(fù)寫 onDraw(Canvas) 方法進行繪制。
// 如果自定義的視圖確實要復(fù)寫該方法,那么需要先調(diào)用 super.draw(canvas)完成系統(tǒng)的繪制,然后再進行自定義的繪制。
    ...
    int saveCount;
    if (!dirtyOpaque) {
          // 步驟1: 繪制本身View背景
        drawBackground(canvas);
    }

        // 如果有必要,就保存圖層(還有一個復(fù)原圖層)
        // 優(yōu)化技巧:
        // 當(dāng)不需要繪制 Layer 時,“保存圖層“和“復(fù)原圖層“這兩步會跳過
        // 因此在繪制的時候,節(jié)省 layer 可以提高繪制效率
        final int viewFlags = mViewFlags;
        if (!verticalEdges && !horizontalEdges) {

        if (!dirtyOpaque) 
             // 步驟2:繪制本身View內(nèi)容  默認(rèn)為空實現(xiàn),  自定義View時需要進行復(fù)寫
            onDraw(canvas);
    
        ......
        // 步驟3:繪制子View   默認(rèn)為空實現(xiàn) 單一View中不需要實現(xiàn),ViewGroup中已經(jīng)實現(xiàn)該方法
        dispatchDraw(canvas);
  
        ........

        // 步驟4:繪制滑動條和前景色等等
        onDrawScrollBars(canvas);

       ..........
        return;
    }
    ...    
}

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

View繪制流程:

五、總結(jié)

從View的測量、布局和繪制原理來看,要實現(xiàn)自定義View,根據(jù)自定義View的種類不同,可能分別要自定義實現(xiàn)不同的方法。但是這些方法不外乎:onMeasure()方法,onLayout()方法,onDraw()方法。

onMeasure()方法:單一View,一般重寫此方法,針對wrap_content情況,規(guī)定View默認(rèn)的大小值,避免于match_parent情況一致。ViewGroup,若不重寫,就會執(zhí)行和單子View中相同邏輯,不會測量子View。一般會重寫onMeasure()方法,循環(huán)測量子View。

onLayout()方法:單一View,不需要實現(xiàn)該方法。ViewGroup必須實現(xiàn),該方法是個抽象方法,實現(xiàn)該方法,來對子View進行布局。

onDraw()方法:無論單一View,或者ViewGroup都需要實現(xiàn)該方法,因其是個空方法

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容