1. 程式人生 > >Android面試收集錄12 View測量、布局及繪制原理

Android面試收集錄12 View測量、布局及繪制原理

模糊 view繪制 params 可能 ec2 androi 流程 https images

一、View繪制的流程框架

技術分享圖片

View的繪制是從上往下一層層叠代下來的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照這個流程從上往下,依次measure(測量),layout(布局),draw(繪制)。

技術分享圖片

二、Measure流程

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

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

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

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

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

由上述流程來看Measure流程很簡單,關鍵點是在於widthMeasureSpec, heightMeasureSpec這兩個參數信息怎麽獲得?

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

MeasureSpec的確定

先介紹下什麽是MeasureSpec?

技術分享圖片

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

其中,Mode模式共分為三類

UNSPECIFIED :不對View進行任何限制,要多大給多大,一般用於系統內部

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

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

那麽MeasureSpec又是如何確定的?

對於DecorView,其確定是通過屏幕的大小,和自身的布局參數LayoutParams。

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

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

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

技術分享圖片

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

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

可能關於MeasureSpec如何確定View大小還有些模糊,篇幅有限,沒詳細具體展開介紹,可以看這篇文章

View的測量流程:

技術分享圖片

三、Layout流程

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

其中布局也是自上而下,不同的是ViewGroup先在layout()中確定自己的布局,然後在onLayout()方法中再調用子View的layout()方法,讓子View布局。在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的布局流程:

技術分享圖片

四、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繪制流程:

技術分享圖片

五、總結

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

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

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

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

六、參考文章

  https://github.com/LRH1993/android_interview/blob/master/android/basis/custom_view.md

Android面試收集錄12 View測量、布局及繪制原理