面試一問:關於 View測量、佈局及繪製原理

前言

2020年2月22.距離新年已經過去了大半個月了,依舊的出不了門,依舊的躲在家裏一日三餐,依舊的在家辦公,也不知道下週會不會復工,再次彙總手中各種保存整理的筆記

相關內容後續GitHub更新,想衝擊金三銀四的小夥伴可以找找看看,歡迎star
順手留下GitHub鏈接,需要獲取相關面試等內容的可以自己去找
https://github.com/xiangjiana/Android-MS
更多完整項目下載。未完待續。源碼。圖文知識後續上傳github。
可以點擊關於我自行查看

一、View繪製的流程框架


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

二、Measure流程

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

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

  public final void measure(int widthMeasureSpec, int heightMeasur eSpec) { 
  }

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

  protected void onMeasure(int widthMeasureSpec, int heightMeasure Spec) { 
     setMeasuredDimension(getDefaultSize(getSuggestedMinimumW idth(), widthMeasureSpec), 
        getDefaultSize(getSuggestedMinimumHeight(), heig htMeasureSpec)); 
   }

由上述流程來看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_parentwrap_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) == PF LAG_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 bot tom) {
     ... 
  // 通過以下賦值語句記錄下了視圖的位置信息,即確定View的四個頂點 
  // 即確定了視圖的位置 
       mLeft = left; 
       mTop = top; 
       mRight = right; 
       mBottom = bottom; 

       mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBott om); 
  }

確定了自身的位置後,就要通過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中不需要實現,ViewG roup中已經實現該方法 
      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都需要實現該方法,因其是個空 方法
自己整理的983頁面試大全,爲打算面試或者正在面試的人提供借鑑的思路

知識彙總的PDF相關內容後續GitHub更新,想衝擊金三銀四的小夥伴可以找找看看,歡迎star
順手留下GitHub鏈接,需要獲取相關面試等內容的可以自己去找
https://github.com/xiangjiana/Android-MS
更多完整項目下載。未完待續。源碼。圖文知識後續上傳github。
可以點擊關於我自行查看

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