《Android開發藝術探索》筆記:第四章 View的工做原理

4.1 初識ViewRoot和DecorView

  • ViewRoot對應於ViewRootImpl類,是鏈接WindowManager和DecorView的紐帶。View的三大流程均是經過ViewRoot來完成的。在ActivityThread中,當Activity對象被建立完畢後,會將DecorView添加到Window中,同時會建立ViewRootImpl對象,並將ViewRootImpl對象和DecorView創建關聯。android

  • View的繪製流程從ViewRoot的performTraversals開始,通過measure、layout和draw三個過程才能夠把一個View繪製出來,其中:canvas

    • measure用來測量View的寬高,
    • layout用來肯定View在父容器中的放置位置,
    • draw則負責將View繪製到屏幕上。
  • performTraversals會依次調用performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程。其中performMeasure中會調用measure方法,在measure方法中又會調用onMeasure方法,在onMeasure方法中則會對全部子元素進行measure過程,這樣就完成了一次measure過程;子元素會重複父容器的measure過程,如此反覆完成了整個View數的遍歷。另外兩個過程相似,大體調用流程以下圖:bash

img

  • measure過程決定了View的寬/高,完成後可經過getMeasuredWidth/getMeasureHeight方法來獲取View測量後的寬/高。
  • Layout過程決定了View的四個頂點的座標和實際View的寬高,完成後可經過getTop、getBotton、getLeft和getRight拿到View的四個定點座標。
  • Draw過程決定了View的顯示,完成後View的內容才能呈現到屏幕上。
  • 以下圖,DecorView做爲頂級View,通常狀況下它內部包含了一個豎直方向的LinearLayout,裏面分爲兩個部分(具體狀況和Android版本和主題有關),上面是標題欄,下面是內容欄。在Activity經過setContextView所設置的佈局文件其實就是被加載到內容欄之中的。
//獲取內容欄
ViewGroup content = findViewById(R.android.id.content);
//獲取咱們設置的Viewcontext.getChildAt(0);
複製代碼

DecorView實際上是一個FrameLayout,View層的事件都先通過DecorView,而後才傳給咱們的View。ide

4.2 理解MeasureSpec

  • MeasureSpec很大程度上決定一個View的尺寸規格,測量過程當中,系統會將View的layoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,再根據這個measureSpec來測量出View的寬/高。oop

  • MeasureSpec表明一個32位的int值,高2位爲SpecMode,低30位爲SpecSize,SpecMode是指測量模式,SpecSize是指在某種測量模式下的規格大小。佈局

  • MpecMode有三類;post

    1.UNSPECIFIED 父容器不對View進行任何限制,要多大給多大,通常用於系統內部 2.EXACTLY 父容器檢測到View所須要的精確大小,這時候View的最終大小就是SpecSize所指定的值,對應LayoutParams中的match_parent具體數值這兩種模式。 3.AT_MOST 父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,不一樣View實現不一樣,對應LayoutParams中的wrap_content優化

  • 當View採用固定寬/高的時候,無論父容器的MeasureSpec的是什麼,View的MeasureSpec都是精確模式而且其大小遵循Layoutparams的大小。動畫

  • 當View的寬/高是match_parent時,若是他的父容器的模式是精確模式,那View也是精確模式而且大小是父容器的剩餘空間;若是父容器是最大模式,那麼View也是最大模式而且起大小不會超過父容器的剩餘空間。spa

  • 當View的寬/高是wrap_content時,無論父容器的模式是精確仍是最大化,View的模式老是最大化而且不能超過父容器的剩餘空間。

補充一個因MeasureSpec致使的問題:解決ScrollView嵌套ListView衝突高度顯示不全問題

自定義一個ListView重寫onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //從新設置高度
    heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製代碼

MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);這個方法就是根據傳入的大小和模式生成一個MeasureSpec類型的32位int值。用兩位來表示模式,剩下30位表示大小。 因此傳入的Integer.MAX_VALUE >> 2就是30位的最大值,模式是MeasureSpec.AT_MOST即表示子視圖最多隻能是specSize中指定的大小,最大不超過這個大小,至關於warp_content,不超過最大size的效果

4.3 View的工做流程

4.3.1.measur過程

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沒有設置背景,返回android:minWidth這個屬性指定的值,能夠是0
    • 若是有背景,則取android:minWidth和背景中的最大值
  • 直接繼承View的自定義控件,須要重寫onMeasure方法而且設置wrap_content時的自身大小,不然在佈局中使用了wrap_content至關於使用了match_parent。
    • 解決方法:在onMeasure時,通過計算,給View指定一個內部寬/高,並在wrap_content時設置便可,其餘狀況沿用系統的測量值便可。

ViewGroup的measure過程

  • 對於ViewGroup來講,除了完成本身的measure過程以外,還會遍歷去調用全部子元素的measure方法,各個子元素再遞歸去執行這個過程。和View不一樣的是,ViewGroup是一個抽象類,沒有重寫View的onMeasure方法,提供了measureChildren方法。
  • measureChildren方法,遍歷獲取子元素,子元素調用measureChild方法
  • measureChild方法,取出子元素的LayoutParams,再經過getChildMeasureSpec方法來建立子元素的MeasureSpec,接着將MeasureSpec傳遞給View的measure方法進行測量。
  • ViewGroup沒有定義其測量的具體過程,由於不一樣的ViewGroup子類有不一樣的佈局特徵(好比LinearLayout和RelativeLayout有不一樣特徵),因此其測量過程的onMeasure方法須要各個子類去具體實現。
  • measure完成以後,經過getMeasureWidth/Height方法就能夠獲取View的測量寬/高,須要注意的是,在某些極端狀況下,系統可能要屢次measure才能肯定最終的測量寬/高,比較好的習慣是在onLayout方法中去獲取測量寬/高或者最終寬/高。

經過LinearLayout的onMeasure方法裏來分析ViewGroup的measure過程:

步驟一:調用onMeasure方法;判斷佈局是水平仍是豎直;根據佈局方向選擇measureVertical或者measureHorizontal方法; 步驟二:進入measureVertical方法,遍歷子元素並對每個子元素執行measureChildBeforeLayout方法,這個方法內部會調用子元素的measure方法;mTotalLength這個變量來存儲LinearLayout在豎直方向上的高度。 步驟三:當子元素測量完畢以後,LinearLayout會根據子元素的狀況來測量本身的大小,若高度採用的是match_parent或者具體值,那麼他的繪製過程和View一致,若採用warp_content,那麼它的高度是全部的子元素所佔用的高度+豎直方向上的Padding。

如何在Activity中獲取View的寬/高信息

由於View的measure過程和Activity的生命週期不是同步進行,若是View尚未測量完畢,那麼獲取到的寬/高就是0;因此在Activity的onCreate、onStart、onResume中均沒法正確的獲取到View的寬/高信息。下面給出4種解決方法。

  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);
複製代碼

4.3.2 layout過程

  • Layout的做用是ViewGroup用來肯定子元素的,當ViewGroup的位置被確認以後,他的layout就會去遍歷全部子元素而且調用onLayout方法,在layout方法中onLayou又被調用,layout方法肯定了View自己的位置,而onLayout方法則會肯定全部子元素的位置
  • View的 layout 方法肯定自己的位置,源碼流程以下:
    • setFrame 肯定View的上下左右四個頂點位置,即肯定了View在父容器中的位置
    • 調用 onLayout 方法,肯定全部子View的位置,和onMeasure同樣,onLayout的具體實現和佈局有關,所以View和ViewGroup均沒有真正實現 onLayout 方法。

以LinearLayout的 onLayout 方法爲例:

  • 遍歷全部子View並調用 setChildFrame 方法來爲子元素指定對應的位置,其中childTop逐漸增大,由於後面的子元素放在靠下的位置。
  • setChildFrame 方法實際上調用了子View的 layout 方法,計算完本身的定位後,經過onLayout方法調用子元素的layout方法讓子元素肯定位置,造成了遞歸,完成View樹的layout過程。

View的測量寬高和最終寬高的區別:

在View的默認實現中,View的測量寬高和最終寬高相等,只不過測量寬高造成於measure過程,最終寬高造成於layout過程。但重寫view的layout方法可使他們不相等。

4.3.3 View的draw過程

  • 將View繪製到屏幕上,大概的幾個步驟: 1.繪製背景background.draw(canvas) 2.繪製本身(onDraw) 3.繪製children(dispatchDraw 遍歷全部子View的 draw 方法 ) 4.繪製前景,滾動條等裝飾(onDrawScrollBars)
  • View的繪製過程是經過dispatchDraw來實現的,它會遍歷全部子元素的draw方法。
  • 若是一個View不須要繪製任何內容,那麼設置setWillNotDraw爲true後,系統會進行相應的優化;ViewGroup默認爲true,若是咱們的自定義ViewGroup須要經過onDraw來繪製內容的時候,須要顯示的關閉它。即調用 setWillNotDraw(false)

4.4 自定義View

4.4.1 自定義View的分類

繼承View 重寫onDraw方法

經過 onDraw 方法來實現一些不規則的效果,這種效果不方便經過佈局的組合方式來達到。這種方式須要本身支持 wrap_content ,而且padding也要去進行處理。

繼承ViewGroup派生特殊的layout

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

繼承特定的View子類( 如TextView、Button)

擴展某種已有的控件的功能,比較簡單,不須要本身去管理 wrap_content 和padding。

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

比較常見,實現幾種view組合一塊兒的效果。

4.4.2 自定義View須知

  • 直接繼承View或ViewGroup的控件, 須要在onmeasure中對wrap_content作特殊處理。指定wrap_content模式下的默認寬/高。
  • 直接繼承View的控件,若是不在draw方法中處理padding,那麼padding屬性就沒法起做用。
  • 直接繼承ViewGroup的控件也須要在onMeasure和onLayout中考慮padding和子元素margin的影響,否則padding和子元素的margin無效。
  • 儘可能不要用在View中使用Handler,由於不必。View內部提供了post系列的方法,徹底能夠替代Handler的做用。
  • View中有線程和動畫,須要在View的onDetachedFromWindow中中止。當View不可見時,也須要中止線程和動畫,不然可能形成內存泄漏。
  • View帶有滑動嵌套情形時,須要處理好滑動衝突
相關文章
相關標籤/搜索