Android View的工作原理
導語
本章主要介紹View的工作原理,可以和Android自定義控制元件對比著看。
主要內容
- 初識ViewRoot和DecorView
- 理解MeasureSpec
- View的工作流程
- 自定義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的繪製流程是從ViewRoot的performTraversals開始的:
- measure用來測量View的寬高
- layout來確定View在父容器中的位置
- draw負責將View繪製在螢幕上
performTraversals會依次呼叫 performMeasure 、 performLayout 和performDraw 三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程。其中 performMeasure 中會呼叫 measure 方法,在 measure 方法中又會呼叫 onMeasure 方法,在 onMeasure 方法中則會對所有子元素進行measure過程,這樣就完成了一次measure過程;子元素會重複父容器的measure過程,如此反覆完成了整個View數的遍歷。另外兩個過程同理。
- Measure完成後, 可以通過getMeasuredWidth 、getMeasureHeight 方法來獲取View測量後的寬/高。特殊情況下,測量的寬高不等於最終的寬高,詳見後面。
- Layout過程決定了View的四個頂點的座標和實際View的寬高,完成後可通過 getTop 、 getBottom 、 getLeft 和 getRight 拿到View的四個定點座標。
DecorView作為頂級View,其實是一個 FrameLayout ,它包含一個豎直方向的 LinearLayout ,這個 LinearLayout 分為標題欄和內容欄兩個部分。
<divalign="center"> <img src="http://images2015.cnblogs.com/blog/500720/201609/500720-20160925174505236-1295369287.png" width = "150" height = "200" alt="圖片" align=center /> </div>
在Activity通過setContextView所設定的佈局檔案其實就是被載入到內容欄之中的。這個內容欄的id是 R.android.id.content ,通過 ViewGroup content = findViewById(R.android.id.content); 可以得到這個contentView。View層的事件都是先經過DecorView,然後才傳遞到子View。
理解MeasureSpec
MeasureSpec決定了一個View的尺寸規格。但是父容器會影響View的MeasureSpec的建立過程。系統將View的 LayoutParams 根據父容器所施加的規則轉換成對應的MeasureSpec,然後根據這個MeasureSpec來測量出View的寬高。
MeasureSpec
MeasureSpec代表一個32位int值,高2位代表SpecMode( 測量模式) ,低30位代表SpecSize( 在某個測量模式下的規格大小)。
SpecMode有三種:
- UNSPECIFIED :父容器不對View進行任何限制,要多大給多大,一般用於系統內部。
- EXACTLY:父容器檢測到View所需要的精確大小,這時候View的最終大小就是SpecSize所指定的值,對應LayoutParams中的 match_parent 和具體數值這兩種模式。
- AT_MOST :對應View的預設大小,不同View實現不同,View的大小不能大於父容器的SpecSize,對應 LayoutParams 中的 wrap_content。
MeasureSpec和LayoutParams的對應關係
對於DecorView,其MeasureSpec由視窗的尺寸和其自身的LayoutParams共同確定。而View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同決定。
View的measure過程由ViewGroup傳遞而來,參考ViewGroup的 measureChildWithMargins 方法,通過呼叫子元素的 getChildMeasureSpec 方法來得到子元素的MeasureSpec,再呼叫子元素的 measure 方法。

規則
- parentSize是指父容器中目前可使用的大小。
當View採用固定寬/高時( 即設定固定的dp/px) ,不管父容器的MeasureSpec是什麼,View的MeasureSpec都是EXACTLY模式,並且大小遵循我們設定的值。 - 當View的寬/高是 match_parent 時,View的MeasureSpec都是EXACTLY模式並且其大小等於父容器的剩餘空間。
- 當View的寬/高是 wrap_content 時,View的MeasureSpec都是AT_MOST模式並且其大小不能超過父容器的剩餘空間。
- 父容器的UNSPECIFIED模式,一般用於系統內部多次Measure時,表示一種測量的狀態,一般來說我們不需要關注此模式。
View的工作流程
measure過程
View的measure過程:
直接繼承View的自定義控制元件需要重寫 onMeasure 方法並設定 wrap_content ( 即specMode是 AT_MOST 模式) 時的自身大小,否則在佈局中使用 wrap_content 相當於使用 match_parent 。對於非 wrap_content 的情形,我們沿用系統的測量值即可。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); // 在 MeasureSpec.AT_MOST 模式下,給定一個預設值mWidth,mHeight。預設寬高靈活指定 //參考TextView、ImageView的處理方式 //其他情況下沿用系統測量規則即可 if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWith, mHeight); } else if (widthSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(mWith, heightSpecSize); } else if (heightSpecMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize, mHeight); } }
ViewGroup的measure過程:
ViewGroup是一個抽象類,沒有重寫View的 onMeasure 方法,但是它提供了一個 measureChildren 方法。這是因為不同的ViewGroup子類有不同的佈局特性,導致他們的測量細節各不相同,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup沒辦法同一實現 onMeasure方法。
measureChildren方法的流程:
- 取出子View的 LayoutParams。
- 通過 getChildMeasureSpec 方法來建立子元素的 MeasureSpec。
- 將 MeasureSpec 直接傳遞給View的measure方法來進行測量。
通過LinearLayout的onMeasure方法裡來分析ViewGroup的measure過程:
- LinearLayout在佈局中如果使用match_parent或者具體數值,測量過程就和View一致,即高度為specSize。
- LinearLayout在佈局中如果使用wrap_content,那麼它的高度就是所有子元素所佔用的高度總和,但不超過它的父容器的剩餘空間。
- LinearLayout的的最終高度同時也把豎直方向的padding考慮在內。
View的measure過程是三大流程中最複雜的一個,measure完成以後,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量後寬/高。在某些情況下,系統可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是準確的。
如果我們想要在Activity啟動的時候就獲取一個View的寬高,怎麼操作呢?因為View的measure過程和Activity的生命週期並不是同步執行,無法保證在Activity的 onCreate、onStart、onResume 時某個View就已經測量完畢。所以有以下四種方式來獲取View的寬高:
- Activity/View#onWindowFocusChanged
onWindowFocusChanged這個方法的含義是:VieW已經初始化完畢了,寬高已經準備好了,需要注意:它會被呼叫多次,當Activity的視窗得到焦點和失去焦點均會被呼叫。 - view.post(runnable)
通過post將一個runnable投遞到訊息佇列的尾部,當Looper呼叫此runnable的時候,View也初始化好了。 - ViewTreeObserver
使用 ViewTreeObserver 的眾多回調可以完成這個功能,比如OnGlobalLayoutListener 這個介面,當View樹的狀態傳送改變或View樹內部的View的可見性發生改變時,onGlobalLayout 方法會被回撥,這是獲取View寬高的好時機。需要注意的是,伴隨著View樹狀態的改變, onGlobalLayout 會被回撥多次。 - view.measure(int widthMeasureSpec,int heightMeasureSpec)
手動對view進行measure。需要根據View的layoutParams分情況處理:
- match_parent:無法measure出具體的寬高,因為不知道父容器的剩餘空間,無法測量出View的大小。
- 具體的數值( dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY); view.measure(widthMeasureSpec,heightMeasureSpec);
- wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); // View的尺寸使用30位二進位制表示,最大值30個1,在AT_MOST模式下,我們用View理論上能支援的最大值去構造MeasureSpec是合理的 int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST); view.measure(widthMeasureSpec,heightMeasureSpec);
layout過程
layout的作用是ViewGroup用來確定子View的位置,當ViewGroup的位置被確定後,它會在onLayout中遍歷所有的子View並呼叫其layout方法,在 layout 方法中, onLayout 方法又會被呼叫。
View的 layout 方法確定本身的位置,原始碼流程如下:
- setFrame 確定View的四個頂點位置,即確定了View在父容器中的位置。
- 呼叫 onLayout 方法,確定所有子View的位置,和onMeasure一樣,onLayout的具體實現和佈局有關,因此View和ViewGroup均沒有真正實現 onLayout 方法。
以LinearLayout的 onLayout 方法為例:
- 遍歷所有子View並呼叫 setChildFrame 方法來為子元素指定對應的位置。
- setChildFrame 方法實際上呼叫了子View的 layout 方法,形成了遞迴。
View的測量寬高和最終寬高的區別:
在View的預設實現中,View的測量寬高和最終寬高相等,只不過測量寬高形成於measure過程,最終寬高形成於layout過程。但重寫view的layout方法可以使他們不相等。
draw過程
View的繪製過程遵循如下幾步:
- 繪製背景 drawBackground(canvas)。
- 繪製自己 onDraw。
- 繪製children dispatchDraw 遍歷所有子View的 draw 方法。
- 繪製裝飾 onDrawScrollBars。
ViewGroup會預設啟用 setWillNotDraw 為ture,導致系統不會去執行 onDraw ,所以自定義ViewGroup需要通過onDraw來繪製內容時,必須顯式的關閉 WILL_NOT_DRAW 這個優化標記位,即呼叫 setWillNotDraw(false)。
自定義View
自定義View的分類
繼承View 重寫onDraw方法:
通過 onDraw 方法來實現一些不規則的效果,這種效果不方便通過佈局的組合方式來達到。這種方式需要自己支援 wrap_content ,並且padding也要去進行處理。
繼承ViewGroup派生特殊的layout:
實現自定義的佈局方式,需要合適地處理ViewGroup的測量、佈局這兩個過程,並同時處理子View的測量和佈局過程。
繼承特定的View子類( 如TextView、Button):
擴充套件某種已有的控制元件的功能,比較簡單,不需要自己去管理 wrap_content 和padding。
繼承特定的ViewGroup子類( 如LinearLayout):
比較常見,實現幾種view組合一起的效果。與方法二的差別是方法二更接近底層實現。
自定義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帶有滑動巢狀情形時,需要處理好滑動衝突。
自定義View例項
- 繼承View重寫onDraw方法: ofollow,noindex">CircleView
- 繼承ViewGroup派生特殊的layout: HorizontalScrollViewEx
onMeasure方法中,首先判斷是否有子元素,沒有的話根據LayoutParams中的寬高做相應處理。然後判斷寬高是不是wrap_content,如果寬是,那麼HorizontalScrollViewEx的寬就是所有所有子元素的寬度之和。如果高是wrap_content,HorizontalScrollViewEx的高度就是第一個子元素的高度。同時要處理padding和margin。
onLayout方法中,在放置子元素時候也要考慮padding和margin。
自定義View的思想
- 掌握基本功,比如View的彈性滑動、滑動衝突、繪製原理等
- 面對新的自定義View時,對其分類並選擇合適的實現思路。