Android組件View繪制流程原理分析
android視圖構(gòu)成
如上圖,Activity的window組成注盈,Activity內(nèi)部有個Window成員庸疾,它的實例為PhoneWindow,PhoneWindow有個內(nèi)部類是DecorView睡互,這個DecorView就是存放布局文件的肠套,里面有TitleActionBar和我們setContentView傳入進(jìn)去的layout布局文件
- Window類時一個抽象類舰涌,提供繪制窗口的API
- PhoneWindow是繼承Window的一個具體的類,該類內(nèi)部包含了一個DecorView對象你稚,該DectorView對象是所有應(yīng)用窗口(Activity界面)的根View
- DecorView繼承FrameLayout瓷耙,里面id=content的就是我們傳入的布局視圖
依據(jù)面向?qū)ο髲某橄蟮骄唧w我們可以類比上面關(guān)系就像如下:
Window是一塊電子屏朱躺,PhoneWindow是一塊手機(jī)電子屏,DecorView就是電子屏要顯示的內(nèi)容搁痛,Activity就是手機(jī)電子屏安裝位置
setContentView流程
setContentView整個過程主要是如何把Activity的布局文件或者java的View添加至窗口里长搀,重點概括為:
創(chuàng)建一個DecorView的對象mDecor,該mDecor對象將作為整個應(yīng)用窗口的根視圖鸡典。
依據(jù)Feature等style theme創(chuàng)建不同的窗口修飾布局文件盈滴,并且通過findViewById獲取Activity布局文件該存放的地方(窗口修飾布局文件中id為content的FrameLayout)。
將Activity的布局文件添加至id為content的FrameLayout內(nèi)轿钠。
當(dāng)setContentView設(shè)置顯示OK以后會回調(diào)Activity的onContentChanged方法。Activity的各種View的findViewById()方法等都可以放到該方法中病苗,系統(tǒng)會幫忙回調(diào)疗垛。
android的View繪制
view繪制主要包括三個方面:
- measure 測量組件本身的大小
- layout 確定組件在視圖中的位置
- draw 根據(jù)位置和大小,將組件畫出來
視圖繪制的起點在ViewRootImpl類的performTraversals()方法硫朦,該方法完成的工作主要是: 根據(jù)之前的狀態(tài)贷腕,判定是否重新計算測試視圖大小(measure)咬展、是佛重新放置視圖位置(layout)和是否重新重繪視圖(draw) 泽裳,部分源碼如下:
private void performTraversals() {
......
//最外層的根視圖的widthMeasureSpec和heightMeasureSpec由來
//lp.width和lp.height在創(chuàng)建ViewGroup實例時等于MATCH_PARENT
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
......
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
......
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
......
mView.draw(canvas);
......
}
measure計算視圖大小
幾乎所有的組件都是繼承View類的,而關(guān)于view的測量工作破婆,日常開發(fā)用得多的方法就是measure和onMeasure兩個方法涮总,measure不可重寫,當(dāng)我們自定義時主要重寫onMeasure方法即可祷舀,在方法內(nèi)部我們必須完成組件的mMeasuredWidth和mMeasuredHeight實際尺寸測量瀑梗,而這個尺寸是需要父視圖和子視圖共同決定的
measure流程從根視圖measure遍歷整個view樹結(jié)構(gòu),如下:
還要注意視圖尺寸MeasureSpec是一個組合尺寸裳扯,它是一個32位bit值抛丽,高兩位是尺寸模式specMode,低30位是尺寸大小值饰豺,我們可以利用提供的原聲庫方法很方便的進(jìn)行尺寸組合和拆解:
specMode有三種: MeasureSpec.EXACTLY表示確定大小亿鲜, MeasureSpec.AT_MOST表示最大大小, MeasureSpec.UNSPECIFIED不確定
int measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); //合成
int specMode = MeasureSpec.getMode(measureSpec); //拆解
int specSize = MeasureSpec.getSize(measureSpec);
而在視圖測量meause中冤吨,父組件傳給子組件的一般都是一個組合尺寸蒿柳,我們可以拿出具體尺寸然后根據(jù)其他條件產(chǎn)生一個新的尺寸值,將這個值用setMeasuredDimension設(shè)置mMeasuredWidth和mMeasuredHeight具體尺寸锅很,完成測量其馏;
measure原理總結(jié)
- MeasureSpec(View的內(nèi)部類)測量規(guī)格為int型,值由高2位規(guī)格模式specMode和低30位具體尺寸specSize組成爆安。其中specMode只有三種值:
MeasureSpec.EXACTLY //確定模式叛复,父View希望子View的大小是確定的,由specSize決定;
MeasureSpec.AT_MOST //最多模式褐奥,父View希望子View的大小最多是specSize指定的值咖耘;
MeasureSpec.UNSPECIFIED //未指定模式,父View完全依據(jù)子View的設(shè)計值來決定撬码;
View的measure方法是final的儿倒,不允許重載,View子類只能重載onMeasure來完成自己的測量邏輯呜笑。
最頂層DecorView測量時的MeasureSpec是由ViewRootImpl中g(shù)etRootMeasureSpec方法確定的(LayoutParams寬高參數(shù)均為MATCH_PARENT夫否,specMode是EXACTLY,specSize為物理屏幕大薪行病)凰慈。
ViewGroup類提供了measureChild,measureChild和measureChildWithMargins方法驼鹅,簡化了父子View的尺寸計算微谓。
只要是ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams,否則無法使用layout_margin參數(shù)输钩。
View的布局大小由父View和子View共同決定豺型。
使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程之后被調(diào)用才能返回有效值买乃。
layout視圖位置確定
layout的流程主要也是遍歷整個view樹結(jié)構(gòu)姻氨,調(diào)用view.layout(int l, int t, int r, int b)確定好view的具體坐標(biāo)位置,流程圖如下
當(dāng)我們自定義一個組件時剪验,通常時重寫onLayout方法哼绑,里面實現(xiàn)好自己的邏輯,最后在調(diào)用layout方法完成視圖位置確定碉咆,如果自定義組件時一個ViewGroup的話抖韩,還需要我們?nèi)ケ闅v每一個child確定尺寸
layout原理總結(jié)
整個layout過程比較容易理解,從上面分析可以看出layout也是從頂層父View向子View的遞歸調(diào)用view.layout方法的過程疫铜,即父View根據(jù)上一步measure子View所得到的布局大小和布局參數(shù)茂浮,將子View放在合適的位置上。具體layout核心主要有以下幾點:
View.layout方法可被重載壳咕,ViewGroup.layout為final的不可重載席揽,ViewGroup.onLayout為abstract的,子類必須重載實現(xiàn)自己的位置邏輯谓厘。
measure操作完成后得到的是對每個View經(jīng)測量過的measuredWidth和measuredHeight幌羞,layout操作完成之后得到的是對每個View進(jìn)行位置分配后的mLeft、mTop竟稳、mRight属桦、mBottom熊痴,這些值都是相對于父View來說的。
凡是layout_XXX的布局屬性基本都針對的是包含子View的ViewGroup的聂宾,當(dāng)對一個沒有父容器的View設(shè)置相關(guān)layout_XXX屬性是沒有任何意義的(前面《Android應(yīng)用setContentView與LayoutInflater加載解析機(jī)制源碼分析》也有提到過)果善。
使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程之后被調(diào)用才能返回有效值系谐。
draw繪制
完成measure和Layout后巾陕,ViewRootImpl中的代碼會創(chuàng)建一個Canvas對象,然后調(diào)用View的draw()方法來執(zhí)行具體的繪制工纪他。所以又回歸到了ViewGroup與View的樹狀遞歸draw過程
先來看下View樹的遞歸draw流程圖鄙煤,如下:
draw原理總結(jié)
可以看見,繪制過程就是把View對象繪制到屏幕上茶袒,整個draw過程需要注意如下細(xì)節(jié):
如果該View是一個ViewGroup馆类,則需要遞歸繪制其所包含的所有子View。
View默認(rèn)不會繪制任何內(nèi)容弹谁,真正的繪制都需要自己在子類中實現(xiàn)。
View的繪制是借助onDraw方法傳入的Canvas類來進(jìn)行的句喜。
區(qū)分View動畫和ViewGroup布局動畫预愤,前者指的是View自身的動畫,可以通過setAnimation添加咳胃,后者是專門針對ViewGroup顯示內(nèi)部子視圖時設(shè)置的動畫植康,可以在xml布局文件中對ViewGroup設(shè)置layoutAnimation屬性(譬如對LinearLayout設(shè)置子View在顯示時出現(xiàn)逐行、隨機(jī)展懈、下等顯示等不同動畫效果)销睁。
在獲取畫布剪切區(qū)(每個View的draw中傳入的Canvas)時會自動處理掉padding,子View獲取Canvas不用關(guān)注這些邏輯存崖,只用關(guān)心如何繪制即可冻记。
默認(rèn)情況下子View的ViewGroup.drawChild繪制順序和子View被添加的順序一致,但是你也可以重載ViewGroup.getChildDrawingOrder()方法提供不同順序来惧。
view提供的API控制視圖的方法
invalidate和postInvalidate方法源碼分析
請求重新繪制視圖冗栗,調(diào)用draw
- invalidate在主線程調(diào)用
- postInvalidate是在非主線程調(diào)用
View的requestLayout方法
requestLayout()方法會調(diào)用measure過程和layout過程,不會調(diào)用draw過程供搀,也不會重新繪制任何View包括該調(diào)用者本身隅居。
本文參考于:這里