View 的繪製過程

sydMobile發表於2019-11-04

宣告

配合Activity 從啟動到佈局繪製的簡單分析 閱讀

View的繪製.png

更多精品文章分類

基本概念介紹

  • Activity:一個 Activity 是一個應用程式元件,提供一個螢幕,使用者可以用來互動。
  • View:所有檢視控制元件的基類
  • ViewGroup:View 的子類,是容器類控制元件,內部用於放置子View
  • Window:概況了 Android 視窗的基本屬性和基本功能(抽象類)
  • PhoneWindow:Window 的實現類
  • DecorView: 介面的 根 View,PhoneWindow 的內部類,FrameLayout 的子類
  • ViewRootImpl:官方定義是 The top of a view hierarchy,implementing the needed protocol between View and the WindowManager. 在 View 層級中的頂層,可以認為是 View 樹的根(注意 ViewRootImpl 不是 View,只是根,DecorView 是根 View,屬於 View)用於串聯 Window 和 View檢視
  • WindowManager:是用來管理視窗的(Window)它的實現物件是 WindowManagerImpl,內部的大部分方法真正的實現是 WindowMangerGlobal
  • WindowManagerService:簡稱 WMS,作用是管理所有應用程式中的視窗

View 的繪製過程

Android頁面來自網路.png

Activity 啟動過程簡單介紹

Activity 設定頁面佈局的過程

  1. 在 ActivityThread 主執行緒中 newActivity 生成一個 Activity

  2. 然後呼叫 Activity 的attach 方法,attach 方法中生成 PhoneWindow 物件

  3. setContentView 中初始化 DecorView (ViewGroup 的子類)其實真正的執行是在 PhoneWindow 中的 setContentView

  4. 在 LayoutInflater 中對佈局檔案進行 xml 解析獲取物件的資料

  5. 根據解析出的資料執行 View 的建構函式進行 View 的建立。

    上面內容是在 onCreate() 中執行完成的

  6. 然後在 onResume 執行完成後呼叫View的繪製

詳細的說明看:Activity 從啟動到佈局繪製的簡單分析

View 的繪製

View 的繪製流程可以分成三步:測量、佈局、繪製

分別對應了:onMeasure() onLayout() onDraw 當然這個過程中也會呼叫許多其他的方法,都是作為輔助,大的流程就這三步。其中這三步內部的執行都是呈現樹狀結構,從根 View 開始迴圈遞進,直到所有子 View 全部執行完畢。

測量 onMeasure

onMeasure(int widthMeasureSpec,int heightMeasureSpec) 這個方法對於單控制元件來說,只是測量他自己,但是對於 ViewGroup 來說還要正確的給它的子控制元件傳入期望的測量數值。然後根據所有子控制元件的大小和 onMeasure 中的引數來設定自己本身的大小。

關於 MeasureSpec 就不多解釋了,這裡只說一下內部的三種模式

  • MeasureSpec.EXACTLY 意思是精確大小,當你想給這個 View 一個精確的大小的時候就是用這個引數,比如佈局中指定了大小是 10 dp 或者 match_parent 都是屬於這種型別的
  • MeasureSpec.AT_MOST 意思是父佈局會給一個最大的值,大小不能超過這個值(就是定義的這種標準,當然你不按照這個標準,在自己寫 onMeasure() 的時候,明明父佈局給的型別是 AT_MOST 你還要超過,那也沒事,只是佈局的樣子可能就會有問題了)。對應 wrap_content 模式
  • MeasureSpec.UNSPECIFIED 意思沒有指定尺寸,這種情況不常見,一般都是父控制元件是 AdapterView 通過 measure 方法傳入模式。

關於 ViewGroup 的自定義,onMeasure() 方法內部需要實現什麼?

首先我們需要給這個控制元件設定正確的期望大小 setMeasuredDimension(width,height) 要想正確的獲取 width 和 height 還需要根據 onMeasure(int widthMeasureSpec,int heightMeasureSpec) 中的引數來確定。如果給的引數型別是 EXACTLY 的話,說明它的父控制元件給他的大小是確定的,這個時候的大小就填寫引數中的數值大小就好(需要 MeasureSpec.getSize(int))。如果引數型別是 AT_MOST 的時候,這個表示父佈局給了一個值,當前的 View 的大小不能超過這個值。那麼我們就需要自己計算出這個 View 想要的大小,然後和父佈局給的最大的值做比較,選擇一個值給 setMeasuredDimension()

那麼如何獲取此 ViewGroup 的正確高度呢?做法就是要獲取到每個子View 的高度和一些 padding Margin 加起來就是這個 ViewGroup 應該的高度了。

要想獲取子 View 的高度就需要呼叫 child.measure() 然後 child.getMeasureHeight 就獲取 Child 的高度了。也就是說需要我們給子 View 測量一下,測量的時候我們需要傳入值。當然這個值也不是隨便傳入的,如果你隨便傳入的話,那麼 child 的大小就亂了,和你在佈局檔案中設定的大小就不一樣了。

那麼如果正確的給 child 傳入值呢?LinearLayout 是這樣做的,當然我們可以根據我們想要的佈局來進行自定義。

// 核心程式碼

// count 是 child 的個數
for(int i=0;i<count;i++){
    // 獲取 child 的 LayoutParmas 這個物件有我們在 xml 中給 view 設定的大小資訊
	final LayoutParams lp = (LayoutParams)child.getLayoutParams();
    // 然後根據 LayoutParams 中的引數和 ViewGroup本身的 widthMeasureSpec 來進行對比,選擇一個合適的數值給
    // child LinearLayout 具體的做法是通過 
    //  ViewGroup 中的 getChildMeasureSpec 方法來獲取一個合適值
    
}


// ViewGroup 中的 getChildMeasureSpec(int spec,int padding,int childDimension) 方法的實現程式碼

// spec 是 onMeasure 中的 spec padding 是子View 的margin + 父控制元件的 padding  childDimension 是子 View 在佈局檔案中給定的大小
public static int getChildMeasureSpec(int spec,int padding,int childDimension){
    int specMode = MeasureSpec.getMode(spec);
    int SpecSize = MeasureSpec.getSize(spec);
    // 得出 ViewGroup 實際可以使用的大小
    int size = Math.max(0,specSize-padding);
    
    int resultSize = 0;
    int resultMode = 0;
    // 然後就是根據 specMode 和 childDimension 來得出合適的大小。
}

複製程式碼

佈局 onLayout

onLayout 對於子控制元件來說沒有什麼意義,對於 ViewGroup 來說,onLayout 方法內部要對子控制元件進行佈局,呼叫子控制元件的 layout 函式。

onLayout 重寫的時候,只需要獲取子 View 的例項,然後呼叫子 View 的 layout 方法來實現佈局就可以了,具體 layout 中傳入的引數,是重寫 onLayout 的重點。需要通過 getMeasureHeight 等獲取子 View 的理想高度,然後再根據具體情況傳入數值。

繪製 onDraw

onDraw() 函式就是來繪製了,一般 ViewGroup 不會實現內部的方法,子控制元件才重寫 onDraw() 方法。也是內部一層層分發繪製。呈現樹狀結構

// 最根部呼叫下面的方法
// public void draw(Canvas canvas);
// 然後此方法內部呼叫 onDraw()(針對於 子View的)dispatchDraw(Canvas canvas) (主要是針對於 ViewGroup 的)
// 然後 dispatchDraw() 內部會呼叫 drawChild(Canvas canvas,View child,long drawingTime) 然後此方法內部會執行 draw 方法,就這樣一層一層下去了。如果最終到了子View就會終止,因為子View dispatchDraw 方法體是空的。

//
複製程式碼

另外可以認為這三個方法都對應著 measure()layout() draw() 方法。可以認為這三個方法內部呼叫了上面的方法。

上面 onMeaure onLayout onDraw() 都介紹完了,那麼最根處的 View 是怎麼呼叫的呢?

佈局樹.png

可以看到上面這張圖,追溯到根View DecorView ,其實最開始就是 ViewRootImp 來呼叫 DecorView 的 measure() ,並且傳入了具體的值,這個值一般就是頁面的大小。然後在 DecorView 的 measure 方法內部會呼叫 onMeasureonMeasure 的內部又會呼叫它的子 View 的 measure 然後就這樣一層層的下去了,直到所有子 View 執行完畢,DecorView 的 measure 就執行完畢了,到此整個頁面的測量工作完成。

onLayout 也是最先 ViewRootImp 來呼叫 DecorView 的 layout() 開始。onDraw 也是最先 ViewRootImp 來呼叫 DecorView 的 draw() 開始的。然後 draw() 的內部的執行就和上面介紹 onDraw() 中一樣了

到此整個頁面的測量、佈局、繪製就全部分析完畢了。

可以檢視:Activity 從啟動到佈局繪製的簡單分析

View 的繪製過程

相關文章