導語
本章主要介紹View的工作原理,可以和Android自定義控件對比著看敦捧。
主要內容
- 初識ViewRoot和DecorView
- 理解MeasureSpec
- View的工作流程
- 自定義View
具體內容
初識ViewRoot和DecorView
ViewRoot的實現是 ViewRootImpl 類,是連接WindowManager和DecorView的紐帶,View的三大流程( measure锅风、layout、draw)均是通過ViewRoot來完成鞍泉。當Activity對象被創(chuàng)建完畢后皱埠,會將DecorView添加到Window中,同時創(chuàng)建 ViewRootImpl 對象塞弊,并將ViewRootImpl 對象和DecorView建立連接漱逸,源碼如下:
root = new ViewRootImpl(view.getContext(),display);
root.setView(view, params, 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 分為標題欄和內容欄兩個部分凛膏。
<div align="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的尺寸規(guī)格鄙麦。但是父容器會影響View的MeasureSpec的創(chuàng)建過程。系統(tǒng)將View的 LayoutParams 根據父容器所施加的規(guī)則轉換成對應的MeasureSpec镊折,然后根據這個MeasureSpec來測量出View的寬高胯府。
MeasureSpec
MeasureSpec代表一個32位int值,高2位代表SpecMode( 測量模式) 恨胚,低30位代表SpecSize( 在某個測量模式下的規(guī)格大新钜颉)。
SpecMode有三種:
- UNSPECIFIED :父容器不對View進行任何限制赃泡,要多大給多大寒波,一般用于系統(tǒng)內部。
- 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模式蔓榄,一般用于系統(tǒng)內部多次Measure時,表示一種測量的狀態(tài)默刚,一般來說我們不需要關注此模式甥郑。
View的工作流程
measure過程
View的measure過程:
直接繼承View的自定義控件需要重寫 onMeasure 方法并設置 wrap_content ( 即specMode是 AT_MOST 模式) 時的自身大小,否則在布局中使用 wrap_content 相當于使用 match_parent 荤西。對于非 wrap_content 的情形澜搅,我們沿用系統(tǒng)的測量值即可。
@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的處理方式
//其他情況下沿用系統(tǒng)測量規(guī)則即可
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子類有不同的布局特性妇萄,導致他們的測量細節(jié)各不相同蜕企,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup沒辦法同一實現 onMeasure方法。
measureChildren方法的流程:
- 取出子View的 LayoutParams冠句。
- 通過 getChildMeasureSpec 方法來創(chuàng)建子元素的 MeasureSpec轻掩。
- 將 MeasureSpec 直接傳遞給View的measure方法來進行測量。
通過LinearLayout的onMeasure方法里來分析ViewGroup的measure過程:
- LinearLayout在布局中如果使用match_parent或者具體數值轩端,測量過程就和View一致放典,即高度為specSize。
- LinearLayout在布局中如果使用wrap_content基茵,那么它的高度就是所有子元素所占用的高度總和奋构,但不超過它的父容器的剩余空間。
- LinearLayout的的最終高度同時也把豎直方向的padding考慮在內拱层。
View的measure過程是三大流程中最復雜的一個弥臼,measure完成以后,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量后寬/高根灯。在某些情況下径缅,系統(tǒng)可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是準確的烙肺。
如果我們想要在Activity啟動的時候就獲取一個View的寬高纳猪,怎么操作呢?因為View的measure過程和Activity的生命周期并不是同步執(zhí)行桃笙,無法保證在Activity的 onCreate氏堤、onStart、onResume 時某個View就已經測量完畢搏明。所以有以下四種方式來獲取View的寬高:
- Activity/View#onWindowFocusChanged
onWindowFocusChanged這個方法的含義是:VieW已經初始化完畢了鼠锈,寬高已經準備好了拘悦,需要注意:它會被調用多次么库,當Activity的窗口得到焦點和失去焦點均會被調用黔姜。 - view.post(runnable)
通過post將一個runnable投遞到消息隊列的尾部耘柱,當Looper調用此runnable的時候,View也初始化好了同欠。 - ViewTreeObserver
使用 ViewTreeObserver 的眾多回調可以完成這個功能样傍,比如OnGlobalLayoutListener 這個接口,當View樹的狀態(tài)發(fā)送改變或View樹內部的View的可見性發(fā)生改變時行您,onGlobalLayout 方法會被回調铭乾,這是獲取View寬高的好時機。需要注意的是娃循,伴隨著View樹狀態(tài)的改變炕檩, 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的測量寬高和最終寬高的區(qū)別:
在View的默認實現中,View的測量寬高和最終寬高相等,只不過測量寬高形成于measure過程截汪,最終寬高形成于layout過程。但重寫view的layout方法可以使他們不相等植捎。
draw過程
View的繪制過程遵循如下幾步:
- 繪制背景 drawBackground(canvas)衙解。
- 繪制自己 onDraw。
- 繪制children dispatchDraw 遍歷所有子View的 draw 方法焰枢。
- 繪制裝飾 onDrawScrollBars蚓峦。
ViewGroup會默認啟用 setWillNotDraw 為ture,導致系統(tǒng)不會去執(zhí)行 onDraw 济锄,所以自定義ViewGroup需要通過onDraw來繪制內容時暑椰,必須顯式的關閉 WILL_NOT_DRAW 這個優(yōu)化標記位,即調用 setWillNotDraw(false)荐绝。
自定義View
自定義View的分類
繼承View 重寫onDraw方法:
通過 onDraw 方法來實現一些不規(guī)則的效果一汽,這種效果不方便通過布局的組合方式來達到。這種方式需要自己支持 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方法:CircleView
- 繼承ViewGroup派生特殊的layout:HorizontalScrollViewEx
onMeasure方法中翎苫,首先判斷是否有子元素,沒有的話根據LayoutParams中的寬高做相應處理榨了。然后判斷寬高是不是wrap_content煎谍,如果寬是,那么HorizontalScrollViewEx的寬就是所有所有子元素的寬度之和龙屉。如果高是wrap_content呐粘,HorizontalScrollViewEx的高度就是第一個子元素的高度。同時要處理padding和margin叔扼。
onLayout方法中事哭,在放置子元素時候也要考慮padding和margin。
自定義View的思想
- 掌握基本功瓜富,比如View的彈性滑動鳍咱、滑動沖突、繪制原理等
- 面對新的自定義View時与柑,對其分類并選擇合適的實現思路谤辜。