參考:
Android應(yīng)用層View繪制流程與源碼分析 - 工匠若水
https://blog.csdn.net/yanbober/article/details/46128379
前言
View在Activity的onCreate()方法中通過setContentView()方法添加到Activity的DecorView上。此時(shí)ViewRootImpl和DecorView沒有關(guān)聯(lián)上奉瘤,不會(huì)繪制View勾拉。在 Activity的onResume()方法執(zhí)行后,通過最終執(zhí)行ViewRootImpl#setView盗温,將DecorView會(huì)被添加帶ViewRootImpl中藕赞。然后執(zhí)行requestlayout(),requestLayout()和invalidate()都會(huì)觸發(fā)ViewRootImpl繪制View => performTraversals()卖局。
Dialog和PopupWindow中View的繪制過程也是一樣的斧蜕,只是觸發(fā)的方式不同。例如Dialog中砚偶,是調(diào)用dialog.show()時(shí)批销,觸發(fā)了WindowManagerImpl的addView()(上圖步驟2),后面的流程就一樣了染坯。
可以通過Activity類的requestWindowFeature()方法來定制Activity關(guān)聯(lián)PhoneWindow的外觀均芽,這個(gè)方法實(shí)際上做的是把我們所請求的窗口外觀特性存儲(chǔ)到了PhoneWindow的mFeatures成員中,在窗口繪制生成外觀模板時(shí)单鹿,根據(jù)mFeatures的值繪制特定外觀骡技。
PhoneWindow中有DecorView對象;
DecorView中有PhoneWindow對象;
ViewRootImpl中有DecorView對象布朦;
ViewRootImpl#performTraversals()
View的繪制從ViewRootImp#performTraversals()開始,該方法在另一個(gè)線程中被執(zhí)行昼窗。該函數(shù)做的執(zhí)行過程主要是根據(jù)之前設(shè)置的狀態(tài)是趴,判斷是否重新計(jì)算視圖大小(measure)、是否重新放置視圖的位置(layout)澄惊、以及是否重繪 (draw)唆途。
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal(); => performTraversals();
}
}
private void performTraversals() {
......
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);
......
}
//mView對于Activty來說就是PhoneWindow.DecorView.
其中,getRootMeasureSpec的兩個(gè)參數(shù)(mWidth, lp.width)mWith和mHeight 是屏幕的寬度和高度掸驱, lp是WindowManager.LayoutParams肛搬,它的lp.width和lp.height的默認(rèn)值是MATCH_PARENT,所以通過getRootMeasureSpec 生成的測量規(guī)格MeasureSpec 的mode是MATCH_PARENT 毕贼,size是屏幕的高寬温赔,即DecorView的MeasureSpec。
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è)計(jì)值來決定;
對于DecorView而言暖庄,它的MeasureSpec是由窗口尺寸和其自身的LayoutParams共同決定聊替;對于普通的View,它的MeasureSpec由父View的MeasureSpec和其自身的LayoutParams共同決定雄驹。
1. measure
父View的measure的過程會(huì)先測量子View佃牛,等子View測量結(jié)果出來后,再來測量自己医舆。
View#onMeasure
onMeasure默認(rèn)的實(shí)現(xiàn)僅僅調(diào)用了setMeasuredDimension俘侠,setMeasuredDimension函數(shù)是一個(gè)很關(guān)鍵的函數(shù),它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值蔬将,measure的主要目的就是對View樹中的每個(gè)View的mMeasuredWidth和mMeasuredHeight進(jìn)行賦值爷速,所以一旦這兩個(gè)變量被賦值意味著該View的測量工作結(jié)束。
setMeasuredDimension=>getDefaultSize
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size; //注意UNSPECIFIED返回的值是getSuggestedMinimumWidth
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize; //系統(tǒng)默認(rèn)的規(guī)格
break;
}
return result;
}
protected int getSuggestedMinimumWidth() {
// 跟背景有關(guān)
return (mBackground == null) ? mMinWidth :
max(mMinWidth, mBackground.getMinimumWidth());
}
ViewGroup#measureChildren
ViewGroup#measureChild
ViewGroup#measureChildWithMargins
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//靜態(tài)方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
注意:int size = Math.max(0, specSize - padding);
MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通過計(jì)算得出一個(gè)針對子View的測量結(jié)果霞怀。其中惫东,子View的LayoutParams其實(shí)就是我們在xml寫的時(shí)候設(shè)置的layout_width和layout_height 轉(zhuǎn)化而來的。
實(shí)際的測量是在onMeasure方法進(jìn)行,所以在View的子類需要重寫onMeasure方法廉沮,這是因?yàn)閙easure方法是final的颓遏,不允許重載,所以View子類只能通過重載onMeasure來實(shí)現(xiàn)自己的測量邏輯滞时。
measure流程圖
measure注意事項(xiàng)
- 直接繼承View的控件需要重寫onMeasure方法并設(shè)置wrap_content時(shí)的自身大小叁幢,否則在布局中使用wrap_content就相當(dāng)于使用match_parent。
- ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams坪稽,否則無法使用layout_margin參數(shù)曼玩。
- 使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個(gè)方法在onMeasure流程之后被調(diào)用才能返回有效值窒百。而getWidth()與getHeight()方法必須在layout(int l, int t, int r, int b)執(zhí)行之后才有效黍判。
2. layout
重載onLayout的目的就是安排其children在父View的具體位置,重載onLayout通常做法就是寫一個(gè)for循環(huán)調(diào)用每一個(gè)子視圖的layout(l, t, r, b)函數(shù)篙梢,傳入不同的參數(shù)l, t, r, b來確定每個(gè)子視圖在父視圖中的顯示位置顷帖。View#layout=>setFrame(int left, int top, int right, int bottom)最終確定view的位置。一般情況下layout過程會(huì)參考measure過程中計(jì)算得到的mMeasuredWidth和mMeasuredHeight來安排子View在父View中顯示的位置庭猩。
整個(gè)layout過程比較容易理解窟她,layout也是從頂層父View向子View的遞歸調(diào)用view.layout方法的過程,即父View根據(jù)上一步measure子View所得到的布局大小和布局參數(shù)蔼水,將子View放在合適的位置上震糖。
layout注意事項(xiàng)
View.layout方法可被重載,ViewGroup.layout為final的不可重載趴腋,ViewGroup.onLayout為abstract的吊说,子類必須重載實(shí)現(xiàn)自己的位置邏輯。
凡是layout_XXX的布局屬性基本都針對的是包含子View的ViewGroup的优炬,當(dāng)對一個(gè)沒有父容器的View設(shè)置相關(guān)layout_XXX屬性是沒有任何意義颁井。
使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個(gè)方法在onLayout流程之后被調(diào)用才能返回有效值蠢护。
3. draw
draw的流程
- 背景繪制
- 對View的內(nèi)容進(jìn)行繪制
- 對當(dāng)前View的所有子View進(jìn)行繪制雅宾,ViewGroup #dispatchDraw(canvas)
- 對View的滾動(dòng)條進(jìn)行繪制
ViewGroup類已經(jīng)為我們實(shí)現(xiàn)繪制子View的默認(rèn)過程,這個(gè)實(shí)現(xiàn)基本能滿足大部分需求葵硕,所以ViewGroup類的子類(LinearLayout,FrameLayout)也基本沒有去重寫dispatchDraw方法眉抬,我們在實(shí)現(xiàn)自定義控件,除非比較特別懈凹,不然一般也不需要去重寫它蜀变, drawChild()的核心過程就是為子視圖分配合適的cavas剪切區(qū),剪切區(qū)的大小正是由layout過程決定的介评,而剪切區(qū)的位置取決于滾動(dòng)值以及子視圖當(dāng)前的動(dòng)畫库北。設(shè)置完剪切區(qū)后就會(huì)調(diào)用子視圖的draw()函數(shù)進(jìn)行具體的繪制了爬舰。
ViewRootImpl中的代碼會(huì)創(chuàng)建一個(gè)Canvas對象。
final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty);
......
mView.draw(canvas);
......
draw注意事項(xiàng)
如果該View是一個(gè)ViewGroup寒瓦,則需要遞歸繪制其所包含的所有子View情屹。View默認(rèn)不會(huì)繪制任何內(nèi)容,真正的繪制都需要自己在子類中實(shí)現(xiàn)杂腰。View的繪制是借助onDraw方法傳入的Canvas類來進(jìn)行的屁商。
區(qū)分View動(dòng)畫和ViewGroup布局動(dòng)畫,前者指的是View自身的動(dòng)畫颈墅,可以通過setAnimation添加,后者是專門針對ViewGroup顯示內(nèi)部子視圖時(shí)設(shè)置的動(dòng)畫雾袱,可以在xml布局文件中對ViewGroup設(shè)置layoutAnimation屬性(譬如對LinearLayout設(shè)置子View在顯示時(shí)出現(xiàn)逐行恤筛、隨機(jī)、下等顯示等不同動(dòng)畫效果)芹橡。
默認(rèn)情況下子View的ViewGroup.drawChild繪制順序和子View被添加的順序一致毒坛,但是你也可以重載ViewGroup.getChildDrawingOrder()方法提供不同順序。
4. invalidate
public void invalidate(boolean invalidateCache) {
invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate) {
......
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
//設(shè)置刷新區(qū)域
damage.set(l, t, r, b);
//傳遞調(diào)運(yùn)Parent ViewGroup的invalidateChild方法
p.invalidateChild(this, damage);
}
......
}
public final void invalidateChild(View child, final Rect dirty) {
ViewParent parent = this;
final AttachInfo attachInfo = mAttachInfo;
......
do {
......
//循環(huán)層層上級調(diào)運(yùn)林说,直到ViewRootImpl會(huì)返回null
parent = parent.invalidateChildInParent(location, dirty);
......
} while (parent != null);
}
View的invalidate(invalidateInternal)方法實(shí)質(zhì)是將要刷新區(qū)域直接傳遞給了父ViewGroup的invalidateChild方法煎殷,在invalidate中,調(diào)用父View的invalidateChild腿箩,這是一個(gè)從當(dāng)前View向上級父View回溯的過程豪直,每一層的父View都將自己的顯示區(qū)域與傳入的刷新Rect做交集。最后傳遞到ViewRootImpl的invalidateChildInParent珠移,方法結(jié)束弓乙。并且在ViewRootImpl#invalidateChildInParent中調(diào)用scheduleTraversals(),scheduleTraversals會(huì)通過Handler的Runnable發(fā)送一個(gè)異步消息钧惧,調(diào)用doTraversal方法击狮,然后最終調(diào)用performTraversals()執(zhí)行重繪绽快。
注意:invalidate方法向上遍歷是在主線程。
invalidate注意事項(xiàng)
- 直接調(diào)用invalidate方法。請求重新draw披坏,但只會(huì)繪制調(diào)用者本身。
- 觸發(fā)setSelection方法缓呛。請求重新draw蛉迹,但只會(huì)繪制調(diào)用者本身。
- 觸發(fā)setVisibility方法钮孵。 當(dāng)View可視狀態(tài)在INVISIBLE轉(zhuǎn)換VISIBLE時(shí)會(huì)間接調(diào)用invalidate方法骂倘,繼而繪制該View。當(dāng)View的可視狀態(tài)在INVISIBLE\VISIBLE 轉(zhuǎn)換為GONE狀態(tài)時(shí)會(huì)間接調(diào)用requestLayout和invalidate方法巴席,同時(shí)由于View樹大小發(fā)生了變化历涝,所以會(huì)請求measure、layout過程以及draw過程,同樣只繪制需要【重新繪制】的視圖荧库。
- 觸發(fā)setEnabled方法堰塌。請求重新draw,但不會(huì)重新繪制任何View包括該調(diào)用者本身分衫。
- 觸發(fā)requestFocus方法场刑。請求View樹的draw過程,只繪制【需要重繪】的View蚪战。
ViewRootImpl類的performTraversals()何時(shí)被調(diào)用牵现?
Activity#setContentView=>mContentParent.addView(view, params);
ViewGroup#addView=>requestLayout()&invalidate(true);
5. requestLayout
public void requestLayout() {
......
if (mParent != null && !mParent.isLayoutRequested()) {
//由此向ViewParent請求布局
//從這個(gè)View開始向上一直requestLayout,最終到達(dá)ViewRootImpl的requestLayout
mParent.requestLayout();
}
}
View的requestLayout時(shí)其實(shí)質(zhì)就是層層向上傳遞邀桑,直到ViewRootImpl為止瞎疼,然后觸發(fā)ViewRootImpl的requestLayout方法,其中調(diào)用 scheduleTraversals();
requestLayout()方法會(huì)調(diào)用measure過程和layout過程壁畸,不會(huì)調(diào)用draw過程贼急,也不會(huì)重新繪制任何View包括該調(diào)用者本身。