手把手教你讀懂源碼寇甸,View的繪制流程詳細(xì)剖析

“勿以善小而不為,勿以惡小而為之疗涉。惟賢惟德拿霉,能服于人≡劭郏”這句出自《三國志·蜀書·先主傳》绽淘,是托孤遺詔的話,婦孺皆知偏窝,卻被這幾天惡作劇了一番收恢。深處如今的畸形直播文化當(dāng)中,前兩天的女主播琪琪的“黃鱔門”祭往,卻以“鱔”小而“慰”之伦意,用其做出不可描述之事,直奔熱搜榜硼补,甚至蓋過中國隊1-0擊敗韓國隊的國足驮肉,在薩德之際不以優(yōu)勢打倒對方,而以弱項讓你屈服已骇!

隨感而發(fā)离钝,瞎扯遠(yuǎn)了票编,還是回歸到我們的正題,我們這一批做技術(shù)的卵渴,不會被外界任何因素干擾慧域,踏踏實(shí)實(shí)潛心修煉,爭取早日成佛浪读。

上一篇文章我們分析了View的加載流程昔榴,今天我們繼續(xù)來深入學(xué)習(xí)View的繪制流程,接著上次的View繪制開始碘橘,同樣使用的是Android 7.1源碼互订。

1、回顧addView方法

上篇文章從addView方法一路分析到了performTraversals()方法痘拆,這個方法非常長仰禽,內(nèi)部邏輯也很復(fù)雜,但是主體邏輯很清晰纺蛆。主要調(diào)用了performMeasure方法吐葵、performLayout方法和performDraw方法:

其執(zhí)行的過程可簡單的概括為:是否需要重新計算視圖的大小(measure)、是否需要重新布局視圖的位置(layout)犹撒,以及是否需要重繪(Draw)折联,也就是我們常說的View的繪制流程。

那么接下來我們一同來詳細(xì)分析一下识颊。

2诚镰、performMeasure

調(diào)用performMeasure之前會先調(diào)用getRootMeasureSpec方法,通過getRootMeasureSpec方法獲得頂層視圖DecorView的測量規(guī)格祥款。

該方法主要作用是在整個窗口的基礎(chǔ)上計算出root view(頂層視圖DecorView)的測量規(guī)格清笨。傳入的兩個參數(shù)分別指:windowSize是當(dāng)前手機(jī)窗口的有效寬和高,一般都是除了通知欄的屏幕寬和高刃跛;rootDimension是根布局DecorView請求的寬和高抠艾,DecorView根布局寬和高都是MATCH_PARENT。

當(dāng)匹配父容器時桨昙,測量模式為MeasureSpec.EXACTLY检号,測量大小直接為屏幕的大小,也就是充滿真?zhèn)€屏幕蛙酪;

當(dāng)包裹內(nèi)容時齐苛,測量模式為MeasureSpec.AT_MOST,測量大小直接為屏幕大小桂塞,也就是充滿真?zhèn)€屏幕凹蜂;

其他情況時,測量模式為MeasureSpec.EXACTLY,測量大小為DecorView頂層視圖布局設(shè)置的大小玛痊。

因此DecorView根布局的測量模式就是MeasureSpec.EXACTLY汰瘫,測量大小一般都是整個屏幕大小,所以一般我們的Activity窗口都是全屏的擂煞。所以上面代碼走第一個分支混弥,然后通過調(diào)用MeasureSpec.makeMeasureSpec方法將DecorView的測量模式和測量大小封裝成DecorView的測量規(guī)格。

該方法只是進(jìn)行了簡單的封裝颈娜。

回到performTraversals方法剑逃,直接來看調(diào)用的performMeasure方法:

該方法調(diào)用了mView的measure()方法浙宜。其中mView是一個View對象官辽,在ViewRootImpl類中的mView是整個UI的根節(jié)點(diǎn),實(shí)際上也就是PhoneWindow中的mDecor對象粟瞬,即一個Activity所對應(yīng)的一個屏幕(不包括頂部的系統(tǒng)狀態(tài)條)中的視圖同仆,包括可能存在也可能不存在的ActionBar嫩码。

繼續(xù)深入查看View的measure方法:

參數(shù)widthMeasureSpec和heightMeasureSpec用來描述當(dāng)前正在處理的視圖可以獲得的最大寬度和高度羔味。

當(dāng)ViewRoot類的成員變量mPrivateFlags的FORCE_LAYOUT位不等于0時啤呼,就表示當(dāng)前視圖正在請求執(zhí)行一次布局操作晶渠,這時候方法就需要重新測量當(dāng)前視圖的寬度和高度沽瘦。此外鸯檬,當(dāng)參數(shù)widthMeasureSpec和heightMeasureSpec的值不等于ViewRoot類的成員變量mldWidthMeasureSpec和mOldHeightMeasureSpec的值時舵稠,就表示當(dāng)前視圖上一次可以獲得的最大寬度和高度已經(jīng)失效了深员,這時候函數(shù)也需要重新測量當(dāng)前視圖的寬度和高度区匠。

當(dāng)View類的measure方法決定要重新測量當(dāng)前視圖的寬度和高度之后干像,它就會首先將成員變量mPrivateFlags的MEASURED_DIMENSION_SET位設(shè)置為0,接著再調(diào)用onMeasure方法來真正執(zhí)行測量寬度和高度的操作驰弄。View類的onMeasure方法執(zhí)行完成之后麻汰,需要再調(diào)用setMeasuredDimension方法來將測量好的寬度和高度設(shè)置到View類的成員變量mMeasuredWidth和mMeasuredHeight中,并且將成員變量mPrivateFlags的EASURED_DIMENSION_SET位設(shè)置為1戚篙。這個操作是強(qiáng)制的五鲫,因?yàn)楫?dāng)前視圖最終就是通過View類的成員變量mMeasuredWidth和mMeasuredHeight來獲得它的寬度和高度的。

繼續(xù)查看View類的onMeasure()方法:

其實(shí)View類的onMeasure方法一般是由其子類來重寫的岔擂。如對于用來應(yīng)用程序窗口的頂層視圖的DecorView類來說位喂,它是通過父類FrameLayout來重寫祖父類View的onMeasure方法的,接下來我們就分析FrameLayout類的onMeasure方法的實(shí)現(xiàn)乱灵。

分析onMeasure方法塑崖,我們先從子類DecorView的onMeasure方法入手,這個方法主要是調(diào)整了兩個入?yún)⒏叨群蛯挾壤龋缓笳{(diào)用其父類的onMeasure方法弃舒。

再看FrameLayout的onMeasure方法,主要是遍歷所有的子View進(jìn)行測量,然后設(shè)置高度聋呢、寬度苗踪。

首先是調(diào)用measureChildWithMargins方法來測量每一個子視圖的寬度和高度,并且找到這些子視圖的最大寬度和高度值削锰,保存在變量maxWidth和maxHeight 中通铲。

接著再將前面得到的寬度maxWidth和高度maxHeight分別加上當(dāng)前視圖所設(shè)置的Padding值,得到的寬度maxWidth和高度maxHeight還不是最終的寬度和高度器贩,還需要考慮以下兩個因素:

1. 當(dāng)前視圖是否設(shè)置有最小寬度和高度颅夺。如果設(shè)置有的話,并且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大蛹稍,那么就將它們作為當(dāng)前視圖的寬度和高度值吧黄。

2. 當(dāng)前視圖是否設(shè)置有前景圖。如果設(shè)置有的話唆姐,并且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大拗慨,那么就將它們作為當(dāng)前視圖的寬度和高度值。

經(jīng)過上述兩步檢查之后奉芦,F(xiàn)rameLayout類的成員函數(shù)onMeasure就得到了當(dāng)前視圖的寬度maxWidth和高度maxHeight赵抢。由于得到的寬度和高度又必須要限制在參數(shù)widthMeasureSpec和heightMeasureSpec所描述的寬度和高度規(guī)范之內(nèi),因此會調(diào)用從View類繼承下來的resolveSizeAndState方法來獲得正確的大小声功。得到了當(dāng)前視圖的正確大小之后烦却,F(xiàn)rameLayout類的onMeasure方法就可以調(diào)用從父類View繼承下來的setMeasuredDimension方法來將它們?yōu)楫?dāng)前視圖的大小了。

我們首先來看一下resolveSizeAndState方法:

該方法把measureSpec入?yún)⒌膍ode和size解析出來先巴,mode封裝在高位中其爵,然后根據(jù)mode來決定最后返回的size。

回到FrameLayout的onMeasure方法筹裕,繼續(xù)分析從父類View繼承下來的setMeasuredDimension方法:

該方法中最關(guān)鍵的步驟是對View的兩個成員變量進(jìn)行一次賦值醋闭,設(shè)置自己所需要的大小。計算的根據(jù)是在xml文件或者代碼中設(shè)置的寬度和高度的參數(shù)朝卒,參數(shù)指明了要求你是填充父控件(match_parent)還是包裹內(nèi)容(wrap_content)還是精確的一個大小证逻,但最終你的大小不應(yīng)該超過父控件給你提供的空間。

onMeasure()方法結(jié)束之前必須調(diào)用setMeasuredDimensionRaw()來設(shè)置View.mMeasuredWidth和View.mMeasuredHeight兩個參數(shù)抗斤。

而當(dāng)這兩個成員變量設(shè)置完成囚企,也就是當(dāng)前的View測量結(jié)束了。

簡單總結(jié)概括一下瑞眼,measure的時序圖如下:

3龙宏、performLayout

繼續(xù)分析ViewRootImpl的performLayout方法:

調(diào)用了根視圖的layout()方法,從傳遞的4個參數(shù)知道DecorView布局的位置是從屏幕最左最頂端開始布局伤疙,到屏幕最低最右結(jié)束银酗。因此DecorView根布局是充滿整個屏幕的辆影。

繼續(xù)分析View類的layout方法:

layout()方法有四個參數(shù),分別是left, top, right, bottom黍特,它們是相對于父控件的位移距離蛙讥。方法里面先調(diào)用了setFrame()方法,該方法非常重要:

該方法先判斷當(dāng)前視圖的大小或者位置是否發(fā)生變化灭衷,將參數(shù)保存起來次慢。當(dāng)前視圖距離父視圖的邊距一旦設(shè)置好之后,它就是一個具有邊界的視圖了翔曲。接下來又會計算當(dāng)前視圖新的寬度newWidth和高度newHeight迫像,如果它們與上一次的寬度oldWidth和oldHeight的值不相等,那么就說明當(dāng)前視圖的大小發(fā)生了變化瞳遍,這時候就會調(diào)用onSizeChanged方法來讓子類有機(jī)會處理這個變化事件闻妓。

繼續(xù)回到layout()方法,后面調(diào)用了onLayout()方法傅蹂,實(shí)際上是給自己的子控件布局纷闺。從以上可以知道m(xù)easure出來的寬度與高度,是該控件期望得到的尺寸份蝴,但是真正顯示到屏幕上的位置與大小是由layout()方法來決定的。left, top決定位置氓轰,right婚夫,bottom決定frame渲染尺寸。

發(fā)現(xiàn)onLayout方法是空的署鸡,直接看DecorView的onLayout方法:

這里先是調(diào)用了FrameLayout的onLayout方法案糙,然后是調(diào)整個別參數(shù)。繼續(xù)看父類FrameLayout的onLayout方法:

直接調(diào)用了調(diào)用了layoutChildren方法靴庆,繼續(xù)分析:

該方法遍歷各個子View时捌,然后調(diào)用子View的layout方法。

需要注意的是FrameLayout布局其實(shí)在View類中的layout方法中已經(jīng)實(shí)現(xiàn)炉抒,布局的邏輯實(shí)現(xiàn)是在父視圖中實(shí)現(xiàn)的奢讨,不像View視圖的measure測量,通過子類實(shí)現(xiàn)onMeasure方法來實(shí)現(xiàn)測量邏輯焰薄。

自定義View一般都無需重寫onMeasure方法拿诸,但是如果自定義一個ViewGroup容器的話,就必須實(shí)現(xiàn)onLayout方法塞茅,因?yàn)樵摲椒ㄔ赩iewGroup是抽象的亩码,所有ViewGroup的所有子類必須實(shí)現(xiàn)onLayout方法。

簡單總結(jié)概括一下野瘦,layout的時序圖如下:

4描沟、performDraw

繼續(xù)分析ViewRootImpl的performDraw方法:

這里面主要看draw方法:

方法結(jié)束前執(zhí)行了drawSoftware方法:

該方法首先獲取需要重繪的位置,鎖定并獲取對應(yīng)的canvas,最后調(diào)用了DecorView的draw方法吏廉。

這里的代碼非常簡單蠢络,調(diào)用了父類的draw方法,以此查找最終定位到了View類的draw方法:

該類非常重要迟蜜,也是最后比較關(guān)鍵的繪制操作刹孔。代碼比較多,但是注釋解釋的非常清楚娜睛,流程具體如下:

1.繪制當(dāng)前視圖的背景髓霞。

2.保存當(dāng)前畫布的堆棧狀態(tài),并且在當(dāng)前畫布上創(chuàng)建額外的圖層畦戒,以便接下來可以用來繪制當(dāng)前視圖在滑動時的邊框漸變效果方库。

3.繪制當(dāng)前視圖的內(nèi)容。

4.繪制當(dāng)前視圖的子視圖的內(nèi)容障斋。

5.繪制當(dāng)前視圖在滑動時的邊框漸變效果纵潦。

6.繪制當(dāng)前視圖的滾動條。

接下來分別分析這個流程垃环,首先來看背景的繪制邀层,非常簡單:

接著是保存畫布canvas的邊框參數(shù)。獲取當(dāng)前視圖View水平或者垂直方向是否需要繪制邊框漸變效果遂庄,如果不需要繪制邊框的漸變效果寥院,就無需執(zhí)行上面的2、5了涛目,那么就直接執(zhí)行上面的3秸谢、4、6步驟霹肝。

假如我們需要繪制視圖View的邊框漸變效果估蹄,那么我們繼續(xù)分析步驟2,3沫换,4臭蚁,5,6苗沧。

這段代碼用來檢查是否需要保存參數(shù)canvas所描述的一塊畫布的堆棧狀態(tài)刊棕,并且創(chuàng)建額外的圖層來繪制當(dāng)前視圖在滑動時的邊框漸變效果。首先需要計算出當(dāng)前視圖的左待逞、右甥角、上以及下內(nèi)邊距的大小,以便得到邊框所要繪制的區(qū)域识樱。

然后接著繪制當(dāng)前視圖的內(nèi)容嗤无,調(diào)用了onDraw方法:

發(fā)現(xiàn)該方法為空震束,主要在子類中實(shí)現(xiàn),繼續(xù)看DecorView的onDraw方法:

當(dāng)前視圖的內(nèi)容繪制完成后当犯,接著繪制子視圖的內(nèi)容垢村,調(diào)用了dispatchDraw方法。

發(fā)現(xiàn)該方法為空嚎卫,真正的實(shí)現(xiàn)在ViewGroup中:

首先判斷當(dāng)前ViewGroup容器是否設(shè)置的布局動畫嘉栓,然后遍歷給每個子視圖View設(shè)置動畫效果,接著獲得布局動畫的控制器拓诸,最后開始布局動畫侵佃。

接下來循環(huán)遍歷每一個子View,并調(diào)用drawChild方法繪制當(dāng)前視圖的子視圖View:

這個draw方法也是View里面的方法奠支,被drawChild()方法調(diào)用:

該方法主要判斷是否有繪制緩存馋辈,如果有直接使用緩存,如果沒有重復(fù)調(diào)用上面的draw()方法倍谜。

然后是第五步迈螟,繪制滑動時的漸變效果:

最后在繪制滾動條:

至此,所有的View對象都繪制出來了尔崔。

需要注意的是:View繪制的畫布參數(shù)canvas是由surface對象獲得答毫,意味著View視圖繪制最終會繪制到Surface對象去。父類View繪制主要是繪制背景您旁、邊框漸變效果烙常、進(jìn)度條,View具體的內(nèi)容繪制調(diào)用了onDraw方法鹤盒,通過該方法把View內(nèi)容的繪制邏輯留給子類去實(shí)現(xiàn)。因此在自定義View的時候都一般都需要重寫父類的onDraw方法來實(shí)現(xiàn)View內(nèi)容繪制侦副。

簡單總結(jié)概括一下侦锯,draw的時序圖如下:

總結(jié)

View的繪制流程是從 ViewRoot 的 performTraversals 方法開始的,它經(jīng)過 measure秦驯、layout尺碰、draw三個過程才最終將一個View繪制出來,performTraversals會依次調(diào)用 performMeasure译隘,performLayout和 performDraw 三個方法亲桥,這三個方法分別會完成 View 的 measure、layout固耘、draw的流程题篷。

在measure方法中,會調(diào)用onMeasure方法厅目,在onMeasure方法中會對所有的子元素進(jìn)行measure過程番枚,這個時候measure流程就從父容器傳遞給子容器法严,這樣就完成了一次測量,接著子元素會重復(fù)父容器的measure的測量過程葫笼,如此反復(fù)的完成整個View樹的過程深啤。同理performLayout的執(zhí)行原理和performDraw的執(zhí)行原理與performMeasure的原理類似。

關(guān)于View的繪制流程路星,經(jīng)常出現(xiàn)在Android面試過程中溯街,同時會嚴(yán)重影響到界面開發(fā)。這一塊理清了洋丐,無論是掌握系統(tǒng)View呈昔,還是自定義View,也或者是解決一些bug垫挨,都有不小的幫助韩肝。

如果還有疑問的童鞋,歡迎留言繼續(xù)討論九榔。


今天就先分享到這里哀峻,后續(xù)將推出更多精彩內(nèi)容,歡迎一起探討學(xué)習(xí)進(jìn)步哲泊。

此文章版權(quán)為微信公眾號分享達(dá)人秀(ShareExpert)——鑫鱻所有剩蟀,若轉(zhuǎn)載請備注出處,特此聲明切威!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末育特,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子先朦,更是在濱河造成了極大的恐慌缰冤,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喳魏,死亡現(xiàn)場離奇詭異棉浸,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)刺彩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門迷郑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人创倔,你說我怎么就攤上這事嗡害。” “怎么了畦攘?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵霸妹,是天一觀的道長。 經(jīng)常有香客問我念搬,道長抑堡,這世上最難降的妖魔是什么摆出? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮首妖,結(jié)果婚禮上偎漫,老公的妹妹穿的比我還像新娘。我一直安慰自己有缆,他們只是感情好象踊,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著棚壁,像睡著了一般杯矩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上袖外,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天史隆,我揣著相機(jī)與錄音,去河邊找鬼曼验。 笑死泌射,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鬓照。 我是一名探鬼主播熔酷,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼豺裆!你這毒婦竟也來了拒秘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤臭猜,失蹤者是張志新(化名)和其女友劉穎躺酒,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蔑歌,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阴颖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了丐膝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡钾菊,死狀恐怖帅矗,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情煞烫,我是刑警寧澤浑此,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站滞详,受9級特大地震影響凛俱,放射性物質(zhì)發(fā)生泄漏紊馏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一蒲犬、第九天 我趴在偏房一處隱蔽的房頂上張望朱监。 院中可真熱鬧,春花似錦原叮、人聲如沸赫编。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽擂送。三九已至,卻和暖如春唯欣,著一層夾襖步出監(jiān)牢的瞬間嘹吨,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工境氢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蟀拷,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓产还,卻偏偏與公主長得像匹厘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子脐区,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容