“勿以善小而不為,勿以惡小而為之疗涉。惟賢惟德拿霉,能服于人≡劭郏”這句出自《三國志·蜀書·先主傳》绽淘,是托孤遺詔的話,婦孺皆知偏窝,卻被這幾天惡作劇了一番收恢。深處如今的畸形直播文化當(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)載請備注出處,特此聲明切威!