Android View的工作原理

導語

本章主要介紹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開始的:

  1. measure用來測量View的寬高
  2. layout來確定View在父容器中的位置
  3. 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 方法十减。

規(guī)則
  • 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方法的流程:

  1. 取出子View的 LayoutParams冠句。
  2. 通過 getChildMeasureSpec 方法來創(chuàng)建子元素的 MeasureSpec轻掩。
  3. 將 MeasureSpec 直接傳遞給View的measure方法來進行測量。

通過LinearLayout的onMeasure方法里來分析ViewGroup的measure過程:

  1. LinearLayout在布局中如果使用match_parent或者具體數值轩端,測量過程就和View一致放典,即高度為specSize。
  2. LinearLayout在布局中如果使用wrap_content基茵,那么它的高度就是所有子元素所占用的高度總和奋构,但不超過它的父容器的剩余空間。
  3. LinearLayout的的最終高度同時也把豎直方向的padding考慮在內拱层。

View的measure過程是三大流程中最復雜的一個弥臼,measure完成以后,通過 getMeasuredWidth/Height 方法就可以正確獲取到View的測量后寬/高根灯。在某些情況下径缅,系統(tǒng)可能需要多次measure才能確定最終的測量寬/高,所以在onMeasure中拿到的寬/高很可能不是準確的烙肺。

如果我們想要在Activity啟動的時候就獲取一個View的寬高纳猪,怎么操作呢?因為View的measure過程和Activity的生命周期并不是同步執(zhí)行桃笙,無法保證在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樹的狀態(tài)發(fā)送改變或View樹內部的View的可見性發(fā)生改變時行您,onGlobalLayout 方法會被回調铭乾,這是獲取View寬高的好時機。需要注意的是娃循,伴隨著View樹狀態(tài)的改變炕檩, onGlobalLayout 會被回調多次。
  4. 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 方法確定本身的位置解寝,源碼流程如下:

  1. setFrame 確定View的四個頂點位置扩然,即確定了View在父容器中的位置。
  2. 調用 onLayout 方法聋伦,確定所有子View的位置夫偶,和onMeasure一樣,onLayout的具體實現和布局有關觉增,因此View和ViewGroup均沒有真正實現 onLayout 方法兵拢。

以LinearLayout的 onLayout 方法為例:

  1. 遍歷所有子View并調用 setChildFrame 方法來為子元素指定對應的位置。
  2. setChildFrame 方法實際上調用了子View的 layout 方法逾礁,形成了遞歸说铃。

View的測量寬高和最終寬高的區(qū)別:
在View的默認實現中,View的測量寬高和最終寬高相等,只不過測量寬高形成于measure過程截汪,最終寬高形成于layout過程。但重寫view的layout方法可以使他們不相等植捎。

draw過程

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

  1. 繪制背景 drawBackground(canvas)衙解。
  2. 繪制自己 onDraw。
  3. 繪制children dispatchDraw 遍歷所有子View的 draw 方法焰枢。
  4. 繪制裝飾 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實例

onMeasure方法中翎苫,首先判斷是否有子元素,沒有的話根據LayoutParams中的寬高做相應處理榨了。然后判斷寬高是不是wrap_content煎谍,如果寬是,那么HorizontalScrollViewEx的寬就是所有所有子元素的寬度之和龙屉。如果高是wrap_content呐粘,HorizontalScrollViewEx的高度就是第一個子元素的高度。同時要處理padding和margin叔扼。
onLayout方法中事哭,在放置子元素時候也要考慮padding和margin。

自定義View的思想
  • 掌握基本功瓜富,比如View的彈性滑動鳍咱、滑動沖突、繪制原理等
  • 面對新的自定義View時与柑,對其分類并選擇合適的實現思路谤辜。

更多內容戳這里(整理好的各種文集)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市价捧,隨后出現的幾起案子丑念,更是在濱河造成了極大的恐慌,老刑警劉巖结蟋,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脯倚,死亡現場離奇詭異,居然都是意外死亡嵌屎,警方通過查閱死者的電腦和手機推正,發(fā)現死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宝惰,“玉大人植榕,你說我怎么就攤上這事∧岫幔” “怎么了尊残?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵炒瘸,是天一觀的道長。 經常有香客問我寝衫,道長顷扩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任慰毅,我火速辦了婚禮屎即,結果婚禮上,老公的妹妹穿的比我還像新娘事富。我一直安慰自己,他們只是感情好乘陪,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布统台。 她就那樣靜靜地躺著,像睡著了一般啡邑。 火紅的嫁衣襯著肌膚如雪贱勃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天谤逼,我揣著相機與錄音贵扰,去河邊找鬼。 笑死流部,一個胖子當著我的面吹牛戚绕,可吹牛的內容都是我干的。 我是一名探鬼主播枝冀,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼舞丛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了果漾?” 一聲冷哼從身側響起球切,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎绒障,沒想到半個月后吨凑,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡户辱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年鸵钝,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焕妙。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蒋伦,死狀恐怖,靈堂內的尸體忽然破棺而出焚鹊,到底是詐尸還是另有隱情痕届,我是刑警寧澤韧献,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站研叫,受9級特大地震影響锤窑,放射性物質發(fā)生泄漏。R本人自食惡果不足惜嚷炉,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一渊啰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧申屹,春花似錦绘证、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至杆煞,卻和暖如春魏宽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背决乎。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工队询, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人构诚。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓蚌斩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親范嘱。 傳聞我的和親對象是個殘疾皇子凳寺,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容