[學習筆記]Android開發藝術探索:View的工作原理

初識ViewRoot和DecorView

ViewRoot的實現是 ViewRootImpl 類,是連接WindowManager和DecorView的紐帶, View的三大流程(mearsure、layout、draw)均是通過ViewRoot來完成。當Activity對象被創建完畢後,會將DecorView添加到Window中,同時創建 ViewRootImpl 對象,並將 ViewRootImpl 對象和DecorView建立連接。

  root = new ViewRootImpl(view.getContext(),display); 
  root.setView(view,wparams, panelParentView);

View的三大流程:

  1. measure用來測量View的寬高

  2. layout來確定View在父容器中的位置

  3. draw負責將View繪製在屏幕上

View的繪製流程從ViewRoot的 performTraversals 方法開始:

  1. performTraversals會依次調用 performMeasure 、 performLayout 和 performDraw 三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程。

  2. 其中 performMeasure 中會調用 measure 方法,在 measure 方法中又會調用 onMeasure 方法,在 onMeasure 方法中則會對所有子元素進行measure過程,這樣就完成了一次measure過程;子元素會重複父容器的measure過程,如此反覆完成了整個View數的遍歷。另外兩個過程同理。

Measure完成後, getMeasuredWidth / getMeasureHeight 方法來獲取View測量後的寬/高。

Layout過程決定了View的四個頂點的座標和實際View的寬高,完成後可通 過 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四個頂點座標。

DecorView作爲頂級View,其實是一個 FrameLayout ,它包含一個豎直方向 的 LinearLayout ,這個 LinearLayout 分爲標題欄和內容欄兩個部分。在Activity通過 setContextView所設置的佈局文件其實就是被加載到內容欄之中的。這個內容欄的id 是 R.android.id.content ,通過 ViewGroup content = findViewById(R.android.id.content); 可以得到這個contentView。View層的事件都是先經 過DecorView,然後才傳遞到子View。

理解MeasureSpec

測量過程,系統將View的 LayoutParams 根據父容器所施加的規則轉換成對應的 MeasureSpec,然後根據這個MeasureSpec來測量出View的寬高。

MeasureSpec代表一個32位int值,高2位代表SpecMode(測量模式),低30位代表 SpecSize(在某個測量模式下的規格大小)。

SpecMode有三種:

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

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

  3. AT_MOST :對應View的默認大小,不同View實現不同,View的大小不能大於父容 器的SpecSize,對應 LayoutParams 中的 wrap_content

View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定。 View的measure過程由ViewGroup傳遞而來,參考ViewGroup的 measureChildWithMargins 方 法,通過調用子元素的 getChildMeasureSpec 方法來得到子元素的MeasureSpec,再調用子元素的 measure 方法。

  1. 當View採用固定寬/高時(即設置固定的dp/px),不管父容器的MeasureSpec是什麼, View的MeasureSpec都是EXACTLY模式,並且大小遵循我們設置的值。
  2. 當View的寬/高是 match_parent 時,View的MeasureSpec都是EXACTLY模式並且其大小等於父容器的剩餘空間。
  3. 當View的寬/高是 wrap_content 時,View的MeasureSpec都是AT_MOST模式並且其大小不能超過父容器的剩餘空間。
  4. 父容器的UNSPECIFIED模式,一般用於系統內部多次Measure時,表示一種測量的狀態,一般來說我們不需要關注此模式。

View的工作流程

measure過程

分兩種情況: 1. View通過 measure 方法就完成了測量過程 2. ViewGroup除了完成自己的測量過程還會便利調用所有子View的 measure 方法,而且各 個子View還會遞歸執行這個過程。

View的measure過程

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          
     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),      widthMeasureSpec),     
     getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
  • setMeasuredDimension方法會設置View的寬/高的測量值
  • getDefaultSize方法返回的大小就是measureSpec中的specSize,也就是View測量後的大小,絕大部分情況和View的最終大小(layout階段確定)相同。
  • getSuggestedMinimumWidth方法,作爲getDefaultSize的第一個參數(建議寬度)

直接繼承View的自定義控件需要重寫 onMeasure 方法並設置 wrap_content (即specMode 是 AT_MOST 模式)時的自身大小,否則在佈局中使用 wrap_content 相當於使 用 match_parent 。對於非 wrap_content 的情形,我們沿用系統的測量值即可。

ViewGroup的measure過程

ViewGroup是一個抽象類,沒有重寫View的 onMeasure 方法,但是它提供了一 個 measureChildren 方法。這是因爲不同的ViewGroup子類有不同的佈局特性,導致他們的測量細節各不相同,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup沒辦法同一實現 onMeasure 方法。

measureChildren方法的流程:

  1. 取出子View的 LayoutParams
  2. 通過 getChildMeasureSpec 方法來創建子元素的 MeasureSpec
  3. 將 MeasureSpec 直接傳遞給View的measure方法來進行測量

View的measure過程是三大流程中最複雜的一個,measure完成以後,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量後寬/高。在某些情況下,系統可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是準確的。同時View的measure過程和Activity的生命週期並不是同步執行,因此無法保證在 Activity的 onCreate、onStart、onResume 時某個View就已經測量完畢。所以有以下四種方式來 獲取View的寬高:

  1. Activity/View#onWindowFocusChanged。 onWindowFocusChanged這個方法的含義 是:VieW已經初始化完畢了,寬高已經準備好了,需要注意:它會被調用多次,當 Activity的窗口得到焦點和失去焦點均會被調用。

  2. view.post(runnable)。 通過post將一個runnable投遞到消息隊列的尾部,當Looper調用此 runnable的時候,View也初始化好了。

  3. ViewTreeObserver。 使用 ViewTreeObserver 的衆多回調可以完成這個功能,比如 OnGlobalLayoutListener 這個接口,當View樹的狀態發送改變或View樹內部的View的 可見性發生改變時, onGlobalLayout 方法會被回調。需要注意的是,伴隨着View樹狀態 的改變, onGlobalLayout 會被回調多次。

  4. view.measure(int widthMeasureSpec,int heightMeasureSpec)。

    (1) match_parent:

    ​ 無法measure出具體的寬高,因爲不知道父容器的剩餘空間,無法測量出View的大小

    (2) 具體的數值(dp/px):

    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
    view.measure(widthMeasureSpec,heightMeasureSpec);
    

    (3) wrap_content:

    int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
    int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
    view.measure(widthMeasureSpec,heightMeasureSpec);
    

layout過程

layout的作用是ViewGroup用來確定子View的位置,當ViewGroup的位置被確定後,它會在 onLayout中遍歷所有的子View並調用其layout方法,在 layout 方法中, onLayout 方法又會被調用。 layout 方法確定View本身的位置,源碼流程如下:

  1. setFrame 確定View的四個頂點位置,即確定了View在父容器中的位置。
  2. 調用 onLayout 方法,確定所有子View的位置。

draw過程

View的繪製過程遵循如下幾步:

  1. 繪製背景 drawBackground(canvas)
  2. 繪製自己 onDraw
  3. 繪製children dispatchDraw 遍歷所有子View的 draw 方法
  4. 繪製裝飾 onDrawScrollBars

View的繪製過程是通過dispatchDraw來實現的,它會遍歷所有子元素的draw方法。

如果一個View不需要繪製任何內容,那麼設置setWillNotDraw爲true後,系統會進行相應的優化;ViewGroup默認爲true,如果我們的自定義ViewGroup需要通過onDraw來繪製內容的時候,需要顯示的關閉它。

自定義View

自定義View的分類

  • 繼承View 通過 onDraw 方法來實現一些效果,需要自己支持 wrap_content ,並且 padding也要去進行處理。

  • 繼承ViewGroup 實現自定義的佈局方式,需要合適地處理ViewGroup的測量、佈局這兩 個過程,並同時處理子View的測量和佈局過程。

  • 繼承特定的View子類(如TextView、Button) 擴展某種已有的控件的功能,且不需要自 己去管理 wrap_content 和padding。

  • 繼承特定的ViewGroup子類(如LinearLayout)

直接繼承View或ViewGroup的控件, 需要在onMeasure中對wrap_content做特殊處理。

直接繼承View的控件,如果不在draw方法中處理padding,那麼padding屬性就無法起作用。直接繼承ViewGroup的控件也需要在onMeasure和onLayout中考慮padding和子元素margin的影響,不然padding和子元素的margin無效。

View內部提供了post系列的方法,完全可以替代Handler的作用。

View中有線程和動畫,需要在View的onDetachedFromWindow中停止。

View帶有滑動嵌套情形時,需要處理好滑動衝突

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