View繪制流程,必問品山,請(qǐng)不要只會(huì)回答onMeasure胆建,onLayout,onDraw肘交,更多完整面試專題笆载,請(qǐng)關(guān)注公眾號(hào)獲取。
?
1涯呻、View繪制的起點(diǎn)
在提升篇(一)中提過凉驻,當(dāng)建立好了decorView與ViewRoot的關(guān)聯(lián)后,ViewRoot類的requestLayout()方法會(huì)被調(diào)用复罐,以完成應(yīng)用程序用戶界面的初次布局涝登。實(shí)際被調(diào)用的是ViewRootImpl類的requestLayout()方法,這個(gè)方法的主要源碼如下:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 檢查發(fā)起布局請(qǐng)求的線程是否為主線程
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
上面的方法中調(diào)用了scheduleTraversals()方法來調(diào)度一次完成的繪制流程市栗,該方法會(huì)向主線程發(fā)送一個(gè)“遍歷”消息缀拭,最終會(huì)導(dǎo)致ViewRootImpl的performTraversals()方法被調(diào)用咳短。下面填帽,我們以performTraversals()為起點(diǎn),來分析View的整個(gè)繪制流程咙好。
2篡腌、View的繪制流程
View的繪制,有三個(gè)步驟:測(cè)量(measure)勾效,布局(layout)嘹悼,繪制(draw), 從DecorView自上而下遍歷整個(gè)View樹,注意是所有View執(zhí)行完一個(gè)步驟后层宫,再進(jìn)行下一步杨伙,而不是一個(gè)View執(zhí)行完所有步驟再遍歷下一個(gè)View。
各步驟的主要工作:
Measure:測(cè)量視圖大小萌腿。從頂層父View到子View遞歸調(diào)用measure方法限匣,measure方法又回調(diào)OnMeasure。
Layout:確定View位置毁菱,進(jìn)行頁面布局米死。從頂層父View向子View的遞歸調(diào)用view.layout方法的過程锌历,即父View根據(jù)上一步measure子View所得到的布局大小和布局參數(shù),將子View放在合適的位置上峦筒。
Draw:繪制視圖究西。ViewRoot創(chuàng)建一個(gè)Canvas對(duì)象,然后調(diào)用OnDraw()物喷。六個(gè)步驟:①卤材、繪制視圖的背景;②峦失、保存畫布的圖層(Layer)商膊;③、繪制View的內(nèi)容宠进;④晕拆、繪制View子視圖,如果沒有就不用材蹬;⑤实幕、還原圖層(Layer);⑥堤器、繪制滾動(dòng)條昆庇。
3、簡(jiǎn)單介紹下MeasureSpec
MeasureSpec由兩部分組成闸溃,一部分是測(cè)量模式整吆,另一部分是測(cè)量的尺寸大小。
其中辉川,Mode模式共分為三類:
EXACTLY:對(duì)應(yīng)LayoutParams中的match_parent和具體數(shù)值這兩種模式表蝙。檢測(cè)到View所需要的精確大小,這時(shí)候View的最終大小就是SpecSize所指定的值乓旗,
AT_MOST :對(duì)應(yīng)LayoutParams中的wrap_content府蛇。View的大小不能大于父容器的大小。
UNSPECIFIED :不對(duì)View進(jìn)行任何限制屿愚,要多大給多大汇跨,一般用于系統(tǒng)內(nèi)部,如ListView妆距,ScrollView
4穷遂、MeasureSpec的確定
這個(gè)沒啥好說的,理解+記憶這個(gè)表格娱据,子View的MeasureSpec由父View根據(jù)自身的MeasureSpec和子View的LayoutParams來共同確定子View的MeasureSpec蚪黑,注意,即使確定了子View的MeasureSpec并不一定決定了子View的大小,自定義View可以根據(jù)需要修改這個(gè)值祠锣,最終通過setMeasuredDimension(width,height)設(shè)置最終大小酷窥。
5、View執(zhí)行onMeasure,onLayout的次數(shù)
分析ViewRootImpl的源碼伴网,scheduleTraversales()內(nèi)部會(huì)執(zhí)行postCallBack觸發(fā)mTraversalRunnable重新走一遍performTraversals(),第二次執(zhí)行performTraversals()就會(huì)觸發(fā)performDraw()蓬推。所以performTraversals()走了兩次,那么肯定會(huì)走2次measure方法澡腾,但不一定走2次onMeasure()沸伏,讀過View measure方法源碼的都應(yīng)知道m(xù)easure方法做了2級(jí)測(cè)量?jī)?yōu)化:
1.如果flag不為forceLayout或者與上次測(cè)量規(guī)格(MeasureSpec)相比未改變,那么將不會(huì)進(jìn)行重新測(cè)量(執(zhí)行onMeasure方法)动分,直接使用上次的測(cè)量值毅糟;
2.如果滿足非強(qiáng)制測(cè)量的條件,即前后二次測(cè)量規(guī)格不一致澜公,會(huì)先根據(jù)目前測(cè)量規(guī)格生成的key索引緩存數(shù)據(jù)姆另,索引到就無需進(jìn)行重新測(cè)量;如果targetSDK小于API 20則二級(jí)測(cè)量?jī)?yōu)化無效,依舊會(huì)重新測(cè)量坟乾,不會(huì)采用緩存測(cè)量值迹辐。
6、getWidth()和getMeasuredWidth()的區(qū)別
getMeasuredWidth()甚侣、getMeasuredHeight()必須在onMeasure之后使用才有效)getMeasuredWidth() 的取值最終來源于 setMeasuredDimension() 方法調(diào)用時(shí)傳遞的參數(shù), getWidth()返回的是明吩,mRight - mLeft,mRight殷费、mLeft 變量分別表示 View 相對(duì)父容器的左右邊緣位置印荔,getWidth()與getHeight()方法必須在layout(int l, int t, int r, int b)執(zhí)行之后才有效
7、如何在onCreate中拿到View的寬度和高度
- View.post(runnable)
view.post(new Runnable() {
@Override
public void run() {
int width = view.getWidth();
int measuredWidth = view.getMeasuredWidth();
Log.i(TAG, "width: " + width);
Log.i(TAG, "measuredWidth: " + measuredWidth);
}
});
利用Handler通信機(jī)制详羡,發(fā)送一個(gè)Runnable到MessageQueue中仍律,當(dāng)View布局處理完成時(shí),自動(dòng)發(fā)送消息殷绍,通知UI進(jìn)程染苛。借此機(jī)制鹊漠,巧妙獲取View的高寬屬性主到,代碼簡(jiǎn)潔,相比ViewTreeObserver監(jiān)聽處理躯概,還不需要手動(dòng)移除觀察者監(jiān)聽事件登钥。
- ViewTreeObserver.addOnGlobalLayoutListener()
監(jiān)聽View的onLayout()繪制過程,一旦layout觸發(fā)變化娶靡,立即回調(diào)onLayoutChange方法牧牢。
注意,使用完也要主要調(diào)用removeOnGlobalListener()方法移除監(jiān)聽事件。避免后續(xù)每一次發(fā)生全局View變化均觸發(fā)該事件塔鳍,影響性能伯铣。
ViewTreeObserver vto = view.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
Log.i(TAG, "width: " + view.getWidth());
Log.i(TAG, "height: " + view.getHeight());
}
});
8、invalidate和postInvalidate區(qū)別
二者都會(huì)出發(fā)刷新View轮纫,并且當(dāng)這個(gè)View的可見性為VISIBLE的時(shí)候腔寡,View的onDraw()方法將會(huì)被調(diào)用,invalidate()方法在 UI 線程中調(diào)用掌唾,重繪當(dāng)前 UI放前。postInvalidate() 方法在非 UI 線程中調(diào)用,通過Handler通知 UI 線程重繪糯彬。
9凭语、requestLayout()的作用
requestLayout()也可以達(dá)到重繪view的目的,但是與前兩者不同撩扒,它會(huì)先調(diào)用onLayout()重新排版似扔,再調(diào)用ondraw()方法。當(dāng)view確定自身已經(jīng)不再適合現(xiàn)有的區(qū)域時(shí)搓谆,該view本身調(diào)用這個(gè)方法要求parent view(父類的視圖)重新調(diào)用他的onMeasure虫几、onLayout來重新設(shè)置自己位置。特別是當(dāng)view的layoutparameter發(fā)生改變挽拔,并且它的值還沒能應(yīng)用到view上時(shí)辆脸,這時(shí)候適合調(diào)用這個(gè)方法requestLayout()。
10螃诅、onDraw() 和dispatchDraw()的區(qū)別
繪制View本身的內(nèi)容啡氢,通過調(diào)用View.onDraw(canvas)函數(shù)實(shí)現(xiàn)
繪制自己的孩子通過dispatchDraw(canvas)實(shí)現(xiàn)
draw過程會(huì)調(diào)用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分發(fā)給子組件進(jìn)行繪制术裸,我們通常定制組件的時(shí)候重寫的是onDraw()方法倘是。值得注意的是ViewGroup容器組件的繪制,當(dāng)它沒有背景時(shí)直接調(diào)用的是dispatchDraw()方法, 而繞過了draw()方法袭艺,當(dāng)它有背景的時(shí)候就調(diào)用draw()方法搀崭,而draw()方法里包含了dispatchDraw()方法的調(diào)用。因此要在ViewGroup上繪制東西的時(shí)候往往重寫的是dispatchDraw()方法而不是onDraw()方法猾编,或者自定制一個(gè)Drawable瘤睹,重寫它的draw(Canvas c)和 getIntrinsicWidth()方法,然后設(shè)為背景答倡。
在這我也分享一份自己收錄整理的Android學(xué)習(xí)PDF+架構(gòu)視頻+面試文檔+源碼筆記轰传,還有高級(jí)架構(gòu)技術(shù)進(jìn)階腦圖、Android開發(fā)面試專題資料瘪撇,高級(jí)進(jìn)階架構(gòu)資料這些都是我閑暇還會(huì)反復(fù)翻閱的精品資料获茬。在腦圖中港庄,每個(gè)知識(shí)點(diǎn)專題都配有相對(duì)應(yīng)的實(shí)戰(zhàn)項(xiàng)目,可以有效的幫助大家掌握知識(shí)點(diǎn)
總之也是在這里幫助大家學(xué)習(xí)提升進(jìn)階恕曲,也節(jié)省大家在網(wǎng)上搜索資料的時(shí)間來學(xué)習(xí)鹏氧,也可以分享給身邊好友一起學(xué)習(xí)
關(guān)注微信公眾號(hào)“Android掃地僧”(微信->添加朋友->公眾號(hào)->輸入“Android掃地僧”)
自動(dòng)回復(fù),即可獲取下載地址