簡介
View的工作流程主要是指measure婿斥、layout民宿、draw這三大流程。其中measure確定View的測量寬高活鹰,layout確定View的最終寬高和四個頂點的位置志群,draw則將View繪制到屏幕上。
View的工作流程入口
在開始三大流程之前荠医,還有一些其他工作,例如將DecorView加載到Window中豫喧。并且三大流程的開始是通過ViewRootImpl來調(diào)用的幢泼。
DecorView被加載到Window中
當(dāng)在Activity的onCreate中調(diào)用setContentView方法時,將會創(chuàng)建DecorView孵班。當(dāng)DecorView創(chuàng)建完畢后招驴,要加載到Window中别厘。這一過程需要從Activity的創(chuàng)建過程說起,先看ActivityThread的handleLaunchActivity方法:
ActivityThread#handleLaunchActivity
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
//...
Activity a = performLaunchActivity(r, customIntent); //該方法會調(diào)用onCreate
if (a != null) {
//...
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);
} else {
//...
}
}
(1)處的performLaunchActivity將會調(diào)用onCreate方法氮发,從而完成DecorView的創(chuàng)建∪吲常現(xiàn)在看handleResumeActivity方法:
ActivityThread#handleResumeActivity
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
//...
r = performResumeActivity(token, clearHide, reason); //調(diào)用onResume
if (r != null) {
final Activity a = r.activity;
//...
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView(); //得到DecorView
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
//...
if (a.mVisibleFromClient && !a.mWindowAdded) {
//由于WindowManager的實現(xiàn)類是WindowManagerImpl披蕉,所以實際
//調(diào)用的是WindowManagerImpl的addView方法
wm.addView(decor, l);
}
}
//...
}
}
繼續(xù)看WindowManagerImpl的addView方法:
WindowManagerImpl#addView
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
//mGlobal是一個WindowManagerGlobal對象
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
繼續(xù)看WindowManagerGlobal的addView方法:
WindowManagerGlobal#addView
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//...
ViewRootImpl root;
synchronized (mLock) {
//...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView);
}
//...
}
}
通過ViewRootImpl的setView方法没讲,ViewRootImpl和DecorView建立聯(lián)系爬凑,并將DecorView加載到Window中。
小結(jié)
在Activity創(chuàng)建過程中贰谣,在onCreate中通過setContentView方法可以完成DecorView的創(chuàng)建吱抚。之后在調(diào)用完onResume后考廉,需要將DecorView加載到Window中。這個過程需要ViewRootImpl的幫助既绕,ViewRootImpl是連接WindowManager和DecorView的橋梁。通過ViewRootImpl的setView方法誓军,ViewRootImpl和DecorView建立聯(lián)系疲扎,并將DecorView加載到Window中椒丧。
開始View的工作流程
通過ViewRootImpl的performTraversals方法開始View的工作流程
ViewRootImpl#performTraversals
private void performTraversals() {
//...
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
//調(diào)用DecorView的measure方法
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
measureAgain = true;
}
//需要重新measure
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
//...
if (didLayout) {
//調(diào)用DecorView的layout方法
performLayout(lp, mWidth, mHeight);
//...
}
//...
if (!cancelDraw && !newSurface) {
//...
//調(diào)用View的draw方法
performDraw();
}
//...
}
可以看到壶熏,在performTraversals方法中,ViewRootImpl先后執(zhí)行了performMeasure溯职、performLayout和performDraw方法帽哑,這三個方法分別調(diào)用了頂級View的measure、layout和draw方法甚带。
measure過程
measure過程要分情況來看佳头,如果是一個View,那么通過measure方法就完成了測量碉输。如果是一個ViewGroup亭珍,那么除了完成自己的測量外肄梨,還要遍歷所有子元素并調(diào)用其measure方法。
View的measure過程
View#measure
View的measure過程由其measure方法開始:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//...
if (forceLayout || needsLayout) {
//...
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
//...
}
//...
}
可以看到侨赡,measure是一個final方法,意味著子類不能重寫該方法蓖宦。measure方法繼續(xù)調(diào)用onMeasure方法:
View#onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
其中油猫,setMeasuredDimension方法設(shè)置View寬高的測量值情妖,getDefaultSize方法得到寬高的測量值。來看下getDefaultSize方法:
View#getDefaultSize
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;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
該方法根據(jù)傳入的MeasureSpec的SpecMode來確定共螺,AT_MOST和EXACTLY模式返回的值都是MeasureSpec的SpecSize情竹。所以一般來說SpecSize就是測量后的大小。
至于UNSPECIFIED模式雏蛮,一般是用于系統(tǒng)內(nèi)部的測量過程阱州。這種情況下寬高的測量值是由getSuggestedMinimumWidth和getSuggestedMinimumHeigh方法決定的苔货。兩個方法同理,這里只分析下getSuggestedMinimumWidth方法:
View#getSuggestedMinimumWidth
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
其中mMinWidth對應(yīng)于android:minWidth屬性指定的值姻灶,如果沒有指定該屬性诈茧,mMinWidth默認(rèn)為0敢会。mBackground.getMinimumWidth()返回的是背景Drawable的原始寬度。
所以該方法的邏輯是:如果沒有設(shè)置背景鸥昏,則返回minWidth屬性對應(yīng)的值互广。否則返回minWidth屬性對應(yīng)的值和背景Drawable的原始寬度中的較大值。
小結(jié)
View的measure過程從measure方法開始像樊,調(diào)用onMeasure方法確定測量寬高旅敷。測量寬高的確定取決于寬高的MeasureSpec。如果SpecMode為AT_MOST或EXACTIY涂滴,測量大小為SpecSize晴音。如果SpecMode為UNSPECIFIED锤躁,需要判斷有無背景,如果沒有設(shè)置背景郭计,測量大小為minWidth屬性對應(yīng)的值椒振;否則測量大小為minWidth屬性對應(yīng)的值和背景Drawable的原始寬度中的較大值。但是UNSPECIFIED模式一般是用于系統(tǒng)內(nèi)部的測量過程庐杨,所以我們平常使用的View的測量大小就是SpecSize夹供。
注意
直接繼承View的自定義控件需要重寫onMeasure()方法罩引,并設(shè)置wrap_content時的自身大小,否則在布局中使用wrap_content就相當(dāng)于使用match_parent揭蜒。
這是因為在當(dāng)View使用wrap_content是剔桨,他的specMode是AT_MOST,而且View的specSize是parentSize,既父容器的當(dāng)前剩余空間大小瑰谜,這與match_parent一致。
如何解決這個問題隐轩?需要重寫onMeasure方法职车,代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
我們只需要給View指定一個寬高(mWidth, mHeight)鹊杖,并在wrap_content時設(shè)置此寬高即可骂蓖。至于這個寬高如何確定,需要根據(jù)View的類型靈活確定登下。對于非wrap_content的情形庐船,我們?nèi)允褂孟到y(tǒng)的測量值。
ViewGroup的measure過程
ViewGroup#measureChildren
ViewGroup是一個抽象類揩瞪,它并沒有重寫onMeasure方法篓冲,而是交由各個實現(xiàn)類來重寫。但是它提供了一個measureChildren方法來測量每個子View嗤攻,實現(xiàn)如下:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
該方法遍歷各子View妇菱,并調(diào)用measureChild方法來測量單個子View
ViewGroup#measureChild
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
該方法先得到子View寬高的MeasureSpec暴区,然后調(diào)用子View的measure方法進行測量
小結(jié)
ViewGrop本身是一個抽象類仙粱,其內(nèi)部并沒有重寫View的onMeasure方法房交,但提供了一個measureChildren()方法來對每一個子元素進行measure。之所以沒有重寫onMeasure方法伐割,是因為ViewGroup的子類具有各種不同的布局特性候味,所以測量方式不同刃唤,這需要子類自己重寫onMeasure方法來定義測量規(guī)則。
注意
View的measure完成之后白群,通過getMeasuredWidth尚胞、getMeasuredHeight方法就可以獲得View的測量寬高。需要注意的是川抡,在某些極端情況下须尚,系統(tǒng)可能要多次measure才能獲得最終的測量寬高崖堤,這時在onMeasure方法獲得的測量寬高可能是不準(zhǔn)確的。一個比較好的習(xí)慣是在onLayout方法中去獲取View的測量寬高耐床。
layout過程
View#layout
由于ViewGroup也是調(diào)用父類View的layout方法密幔,所有先從View的layout方法看起:
public void layout(int l, int t, int r, int b) {
//...
//初始化四個頂點的值
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//如果視圖的大小和位置發(fā)生變化,調(diào)用onLayout
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
//...
}
//...
}
該方法的步驟是:先初始化四個頂點的值撩轰,確定View在其父容器中的位置胯甩。如果發(fā)現(xiàn)View的大小和位置發(fā)生了變化,會繼續(xù)調(diào)用onLayout方法確定子元素的位置堪嫂。
View和ViewGroup都沒有實現(xiàn)onLayout偎箫,而是交由具體的ViewGroup來實現(xiàn)。下面看下LinearLayout的onLayout方法:
LinearLayout#onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
只看豎向排列的情況皆串,調(diào)用layoutVertical方法
LinearLayout#layoutVertical
void layoutVertical(int left, int top, int right, int bottom) {
//...
final int count = getVirtualChildCount();
//...
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//...
//確定子元素的位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
該方法遍歷子元素淹办,并通過setChildFrame方法確定子元素的位置
LinearLayout#setChildFrame
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
可以看出,setChildFrame方法又是調(diào)用子元素的layout方法來確定子元素的位置
小結(jié)
View只需確定自己四個頂點的位置即可確定自己的位置恶复,而ViewGroup除了要確定自己的位置怜森,如果發(fā)現(xiàn)自己的大小和位置發(fā)生了變化,還要調(diào)用onLayout重新確定子元素的位置谤牡。而在確定子元素位置的時候副硅,又會調(diào)用其layout方法,直到所有的View都確定位置翅萤。
注意
在View的默認(rèn)實現(xiàn)中恐疲,View的測量寬高和最終寬高是相等的,只不過測量寬高形成于View的measure過程套么,而最終寬高形成于View的layout過程流纹。因此,一般情況下违诗,我們可以認(rèn)為View的測量寬高等于最終寬高漱凝,但是在某些特殊情況會導(dǎo)致兩者不一樣:
第一種情況如下:
@Override
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100, b + 100);
}
上面重寫了View的layout方法,將導(dǎo)致會View的最終寬高比測量寬高大100px诸迟,雖然這樣做會導(dǎo)致View顯示不正常并且也沒有實際意義茸炒。
另一種情況是在某些情況愕乎,View需要多次measure才能確定自己的測量寬高,那么可能前幾次得出的測量寬高和最終寬高不一致壁公,但最終的測量寬高還是和最終寬高相同感论。
draw過程
View#draw
由于ViewGroup并沒有重寫draw方法,所以只需看View的draw方法即可:
public void draw(Canvas canvas) {
//...
//繪制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
//...
if (!verticalEdges && !horizontalEdges) {
//繪制自己
if (!dirtyOpaque) onDraw(canvas); //View的onDraw方法是一個空方法紊册,需要子類自己實現(xiàn)
//繪制子元素
dispatchDraw(canvas); //在View中是一個空方法比肄,ViewGroup重寫了該方法
//繪制裝飾(foreground, scrollbars)
onDrawForeground(canvas);
//...
}
//...
}
可以看出,View的繪制過程步驟如下:
- 繪制背景:調(diào)用背景Drawable的draw方法
- 繪制自己:調(diào)用onDraw方法囊陡,這是一個空方法芳绩,需要子類自己實現(xiàn)
- 繪制子元素:調(diào)用dispatchDraw方法,該方法在View中是一個空方法撞反,ViewGroup重寫了該方法
- 繪制裝飾:調(diào)用onDrawForeground方法妥色,繪制foreground, scrollbars等
注意
View有一個特殊的方法setWillNotDraw:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
從注釋可以看出,如果一個View不需要繪制任何內(nèi)容遏片,那么設(shè)置這個標(biāo)志位為true后嘹害,系統(tǒng)會進行相應(yīng)的優(yōu)化。默認(rèn)情況下吮便,View沒有啟用這個標(biāo)志位笔呀,但是ViewGroup會默認(rèn)啟用這個優(yōu)化標(biāo)志位。
這個標(biāo)志位的意義是:當(dāng)我們自定義的控件繼承與ViewGroup并且自身不具備繪制功能(沒有重寫onDraw)時髓需,就可以開啟這個標(biāo)志位從而便于系統(tǒng)進行后續(xù)的優(yōu)化许师。相反,當(dāng)我們需要重寫ViewGroup的onDraw方法來繪制內(nèi)容時授账,就需要顯示地關(guān)閉這個標(biāo)志位枯跑。
參考
- 《Android 開發(fā)藝術(shù)探索》
- 《Android 進階之光》