深入理解 Android 之 View 的繪製流程
概述
本篇文章會從原始碼(基於Android 6.0
)角度分析Android
中View
的繪製流程,側重於對整體流程的分析,對一些難以理解的點加以重點闡述,目的是把View
繪製的整個流程把握好,而對於特定實現細節則可以日後再對相應原始碼進行研讀。
在進行實際的分析之前,我們先來看下面這張圖:
我們來對上圖做出簡單解釋:DecorView
是一個應用視窗的根容器,它本質上是一個FrameLayout
。DecorView
有唯一一個子View
,它是一個垂直LinearLayout
,包含兩個子元素,一個是TitleView
(ActionBar
的容器),另一個是ContentView
(視窗內容的容器)。關於ContentView
,它是一個FrameLayout
(android.R.id.content
),我們平常用的setContentView
就是設定它的子View
。上圖還表達了每個Activity
都與一個Window
(具體來說是PhoneWindow
)相關聯,使用者介面則由Window
所承載。
Window
Window
即視窗,這個概念在Android Framework
中的實現為android.view.Window
這個抽象類,這個抽象類是對Android系統中的視窗的抽象。在介紹這個類之前,我們先來看看究竟什麼是視窗呢?
實際上,視窗是一個巨集觀的思想,它是螢幕上用於繪製各種UI元素及響應使用者輸入事件的一個矩形區域。通常具備以下兩個特點:
- 獨立繪製,不與其它介面相互影響;
- 不會觸發其它介面的輸入事件;
在Android系統中,視窗是獨佔一個Surface
例項的顯示區域,每個視窗的Surface
由WindowManagerService
分配。我們可以把Surface
看作一塊畫布,應用可以通過Canvas
或OpenGL
在其上面作畫。畫好之後,通過SurfaceFlinger
將多塊Surface
按照特定的順序(即Z-order)進行混合,而後輸出到FrameBuffer
中,這樣使用者介面就得以顯示。
android.view.Window
這個抽象類可以看做Android中對視窗這一巨集觀概念所做的約定,而PhoneWindow
這個類是Framework
為我們提供的Android視窗概念的具體實現。接下來我們先來介紹一下android.view.Window
這個抽象類。
這個抽象類包含了三個核心元件:
- WindowManager.LayoutParams: 視窗的佈局引數;
- Callback: 視窗的回撥介面,通常由Activity實現;
- ViewTree: 視窗所承載的控制元件樹。
下面我們來看一下Android
中Window的具體實現(也是唯一實現)——PhoneWindow
。
PhoneWindow
前面我們提到了,PhoneWindow這個類是Framework為我們提供的Android視窗的具體實現。我們平時呼叫setContentView()方法設定Activity的使用者介面時,實際上就完成了對所關聯的PhoneWindow的ViewTree的設定。我們還可以通過Activity類的requestWindowFeature()方法來定製Activity關聯PhoneWindow的外觀,這個方法實際上做的是把我們所請求的視窗外觀特性儲存到了PhoneWindow的mFeatures成員中,在視窗繪製階段生成外觀模板時,會根據mFeatures的值繪製特定外觀。
從setContentView()
說開去
在分析setContentView()
方法前,我們需要明確:這個方法只是完成了Activity
的ContentView
的建立,而並沒有執行View
的繪製流程。
當我們自定義Activity
繼承自android.app.Activity
時候,呼叫的setContentView()
方法是Activity
類的,原始碼如下:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
. . .
}
getWindow()
方法會返回Activity所關聯的PhoneWindow
,也就是說,實際上呼叫到了PhoneWindow
的setContentView()
方法,原始碼如下:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// mContentParent即為上面提到的ContentView的父容器,若為空則呼叫installDecor()生成
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 具有FEATURE_CONTENT_TRANSITIONS特性表示開啟了Transition
// mContentParent不為null,則移除decorView的所有子View
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
// 開啟了Transition,做相應的處理,我們不討論這種情況
// 感興趣的同學可以參考原始碼
. . .
} else {
// 一般情況會來到這裡,呼叫mLayoutInflater.inflate()方法來填充佈局
// 填充佈局也就是把我們設定的ContentView加入到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
. . .
// cb即為該Window所關聯的Activity
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 呼叫onContentChanged()回撥方法通知Activity視窗內容發生了改變
cb.onContentChanged();
}
. . .
}
LayoutInflater.inflate()
在上面我們看到了,PhoneWindow
的setContentView()
方法中呼叫了LayoutInflater
的inflate()
方法來填充佈局,這個方法的原始碼如下:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
. . .
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
在PhoneWindow
的setContentView()
方法中傳入了decorView
作為LayoutInflater.inflate()
的root
引數,我們可以看到,通過層層呼叫,最終呼叫的是inflate(XmlPullParser, ViewGroup, boolean)
方法來填充佈局。這個方法的原始碼如下:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
. . .
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
// Look for the root node.
int type;
// 一直讀取xml檔案,直到遇到開始標記
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
// 最先遇到的不是開始標記,報錯
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
final String name = parser.getName();
. . .
// 單獨處理<merge>標籤,不熟悉的同學請參考官方文件的說明
if (TAG_MERGE.equals(name)) {
// 若包含<merge>標籤,父容器(即root引數)不可為空且attachRoot須為true,否則報錯
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
// 遞迴地填充佈局
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// temp為xml佈局檔案的根View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
. . .
// 獲取父容器的佈局引數(LayoutParams)
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 若attachToRoot引數為false,則我們只會將父容器的佈局引數設定給根View
temp.setLayoutParams(params);
}
}
// 遞迴載入根View的所有子View
rInflateChildren(parser, temp, attrs, true);
. . .
if (root != null && attachToRoot) {
// 若父容器不為空且attachToRoot為true,則將父容器作為根View的父View包裹上來
root.addView(temp, params);
}
// 若root為空或是attachToRoot為false,則以根View作為返回值
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
. . .
} catch (Exception e) {
. . .
} finally {
. . .
}
return result;
}
}
在上面的原始碼中,首先對於佈局檔案中的<merge>
標籤進行單獨處理,呼叫rInflate()
方法來遞迴填充佈局。這個方法的原始碼如下:
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
// 獲取當前標記的深度,根標記的深度為0
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
// 不是開始標記則繼續下一次迭代
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
// 對一些特殊標記做單獨處理
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
// 對<include>做處理
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
// 對一般標記的處理
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params=viewGroup.generateLayoutParams(attrs);
// 遞迴地載入子View
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) {
parent.onFinishInflate();
}
}
我們可以看到,上面的inflate()和rInflate()方法中都呼叫了rInflateChildren()方法,這個方法的原始碼如下:
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
從原始碼中我們可以知道,rInflateChildren()
方法實際上呼叫了rInflate()
方法。
到這裡,setContentView()
的整體執行流程我們就分析完了,至此我們已經完成了Activity
的ContentView
的建立與設定工作。接下來,我們開始進入正題,分析View
的繪製流程。
ViewRoot
在介紹View
的繪製前,首先我們需要知道是誰負責執行View
繪製的整個流程。實際上,View
的繪製是由ViewRoot
來負責的。每個應用程式視窗的decorView
都有一個與之關聯的ViewRoot
物件,這種關聯關係是由WindowManager
來維護的。
那麼decorView
與ViewRoot
的關聯關係是在什麼時候建立的呢?答案是Activity啟時,ActivityThread.handleResumeActivity()
方法中建立了它們兩者的關聯關係。這裡我們不具體分析它們建立關聯的時機與方式,感興趣的同學可以參考相關原始碼。下面我們直入主題,分析一下ViewRoot
是如何完成View
的繪製的。
View繪製的起點
當建立好了decorView
與ViewRoot
的關聯後,ViewRoot
類的requestLayout()
方法會被呼叫,以完成應用程式使用者介面的初次佈局。實際被呼叫的是ViewRootImpl
類的requestLayout()
方法,這個方法的原始碼如下:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
// 檢查發起佈局請求的執行緒是否為主執行緒
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
上面的方法中呼叫了scheduleTraversals()
方法來排程一次完成的繪製流程,該方法會向主執行緒傳送一個“遍歷”訊息,最終會導致ViewRootImpl
的performTraversals()
方法被呼叫。下面,我們以performTraversals()
為起點,來分析View的整個繪製流程。
三個階段
View
的整個繪製流程可以分為以下三個階段:
measure
: 判斷是否需要重新計算View的大小,需要的話則計算;layout
: 判斷是否需要重新計算View的位置,需要的話則計算;draw
: 判斷是否需要重新繪製View,需要的話則重繪製。
這三個子階段可以用下圖來描述:
measure階段
此階段的目的是計算出控制元件樹中的各個控制元件要顯示其內容的話,需要多大尺寸。起點是ViewRootImpl
的measureHierarchy()
方法,這個方法的原始碼如下:
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res,
final int desiredWindowWidth, final int desiredWindowHeight) {
// 傳入的desiredWindowXxx為視窗尺寸
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
. . .
boolean goodMeasure = false;
if (!goodMeasure) {
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
return windowSizeMayChange;
}
上面的程式碼中呼叫getRootMeasureSpec()
方法來獲取根MeasureSpec
,這個根MeasureSpec
代表了對decorView
的寬高的約束資訊。繼續分析之前,我們先來簡單地介紹下MeasureSpec
的概念。
MeasureSpec
是一個32位整數,由SpecMode
和SpecSize
兩部分組成,其中,高2位為SpecMode
,低30位為SpecSize
。SpecMode
為測量模式,SpecSize
為相應測量模式下的測量尺寸。View
(包括普通View
和ViewGroup
)的SpecMode
由本View
的LayoutParams
結合父View
的MeasureSpec
生成。
SpecMode
的取值可為以下三種:
EXACTLY
: 對子View提出了一個確切的建議尺寸(SpecSize);AT_MOST
: 子View的大小不得超過SpecSize;UNSPECIFIED
: 對子View的尺寸不作限制,通常用於系統內部。
傳入performMeasure()
方法的MeasureSpec
的SpecMode
為EXACTLY
,SpecSize
為視窗尺寸。
performMeasure()
方法的原始碼如下:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
. . .
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
. . .
}
}
上面程式碼中的mView
即為decorView
,也就是說會轉向對View.measure()
方法的呼叫,這個方法的原始碼如下:
/**
* 呼叫這個方法來算出一個View應該為多大。引數為父View對其寬高的約束資訊。
* 實際的測量工作在onMeasure()方法中進行
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
. . .
// 判斷是否需要重新佈局
// 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT標記,則強制重新佈局
// 比如呼叫View.requestLayout()會在mPrivateFlags中加入此標記
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
|| heightMeasureSpec != mOldHeightMeasureSpec;
final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
&& getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
final boolean needsLayout = specChanged
&& (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
// 需要重新佈局
if (forceLayout || needsLayout) {
. . .
// 先嚐試從緩從中獲取,若forceLayout為true或是快取中不存在或是
// 忽略快取,則呼叫onMeasure()重新進行測量工作
int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
. . .
} else {
// 快取命中,直接從快取中取值即可,不必再測量
long value = mMeasureCache.valueAt(cacheIndex);
// Casting a long to int drops the high 32 bits, no mask needed
setMeasuredDimensionRaw((int) (value >> 32), (int) value);
. . .
}
. . .
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
從measure()
方法的原始碼中我們可以知道,只有以下兩種情況之一,才會進行實際的測量工作:
forceLayout
為true
:這表示強制重新佈局,可以通過View.requestLayout()
來實現;needsLayout
為true
,這需要specChanged
為true
(表示本次傳入的MeasureSpec
與上次傳入的不同),並且以下三個條件之一成立:sAlwaysRemeasureExactly
為true
: 該變數預設為false
;isSpecExactly
為false
: 若父View
對子View
提出了精確的寬高約束,則該變數為true
,否則為false
matchesSpecSize
為false
: 表示父View
的寬高尺寸要求與上次測量的結果不同
對於decorView
來說,實際執行測量工作的是FrameLayout
的onMeasure()
方法,該方法的原始碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
. . .
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
. . .
}
}
// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
. . .
}
FrameLayout
是ViewGroup
的子類,後者有一個View[]
型別的成員變數mChildren
,代表了其子View
集合。通過getChildAt(i)
能獲取指定索引處的子View
,通過getChildCount()
可以獲得子View
的總數。
在上面的原始碼中,首先呼叫measureChildWithMargins()
方法對所有子View
進行了一遍測量,並計算出所有子View
的最大寬度和最大高度。而後將得到的最大高度和寬度加上padding
,這裡的padding
包括了父View
的padding
和前景區域的padding
。然後會檢查是否設定了最小寬高,並與其比較,將兩者中較大的設為最終的最大寬高。最後,若設定了前景影象,我們還要檢查前景影象的最小寬高。
經過了以上一系列步驟後,我們就得到了maxHeight
和maxWidth
的最終值,表示當前容器View
用這個尺寸就能夠正常顯示其所有子View
(同時考慮了padding
和margin
)。而後我們需要呼叫resolveSizeAndState()
方法來結合傳來的MeasureSpec
來獲取最終的測量寬高,並儲存到mMeasuredWidth
與mMeasuredHeight
成員變數中。
從以上程式碼的執行流程中,我們可以看到,容器View
通過measureChildWithMargins()
方法對所有子View
進行測量後,才能得到自身的測量結果。也就是說,對於ViewGroup
及其子類來說,要先完成子View
的測量,再進行自身的測量(考慮進padding
等)。
接下來我們來看下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);
}
由以上程式碼我們可以知道,對於ViewGroup
來說,它會呼叫child.measure()
來完成子View的測量。傳入ViewGroup
的MeasureSpec是它的父View
用於約束其測量的,那麼ViewGroup
本身也需要生成一個childMeasureSpec
來限制它的子View
的測量工作。這個childMeasureSpec就由getChildMeasureSpec()方法生成。接下來我們來分析這個方法:
上面的方法展現了根據父View的MeasureSpec
和子View的LayoutParams
生成子View的MeasureSpec
的過程, 子View的LayoutParams
表示了子View的期待大小。這個產生的MeasureSpec
用於指導子View自身的測量結果的確定。
在上面的程式碼中,我們可以看到當ParentMeasureSpec
的SpecMode
為EXACTLY
時,表示父View對子View指定了確切的寬高限制。此時根據子View的LayoutParams
的不同,分以下三種情況:
- 具體大小(
childDimension
):這種情況下令子View的SpecSize
為childDimension,即子View在LayoutParams
指定的具體大小值;令子View的SpecMode
為EXACTLY
,即這種情況下若該子View為容器View,它也有能力給其子View指定確切的寬高限制(子View只能在這個寬高範圍內),若為普通View,它的最終測量大小就為childDimension。 match_parent
:此時表示子View想和父View一樣大。這種情況下得到的子View的SpecMode與上種情況相同,只不過SpecSize為size,即父View的剩餘可用大小。wrap_content
:這表示了子View想自己決定自己的尺寸(根據其內容的大小動態決定)。這種情況下子View的確切測量大小隻能在其本身的onMeasure()方法中計算得出,父View此時無從知曉。所以暫時將子View的SpecSize設為size(父View的剩餘大小);令子View的SpecMode為AT_MOST,表示了若子View為ViewGroup,它沒有能力給其子View指定確切的寬高限制,畢竟它本身的測量寬高還懸而未定。
當ParentMeasureSpec
的SpecMode
為AT_MOST
時,我們也可以根據子View
的LayoutParams
的不同來分三種情況討論:
- 具體大小:這時令子View的
SpecSize
為childDimension
,SpecMode
為EXACTLY
。 - match_parent:表示子View想和父View一樣大,故令子View的SpecSize為size,但是由於父View本身的測量寬高還無從確定,所以只是暫時令子View的測量結果為父View目前的可用大小。這時令子View的SpecMode為AT_MOST。
- wrap_content:表示子View想自己決定大小(根據其內容動態確定)。然而這時父View還無法確定其自身的測量寬高,所以暫時令子View的SpecSize為size,SpecMode為AT_MOST。
從上面的分析我們可以得到一個通用的結論,當子View的測量結果能夠確定時,子View的SpecMode就為EXACTLY;當子View的測量結果還不能確定(只是暫時設為某個值)時,子View的SpecMode為AT_MOST。
在measureChildWithMargins()
方法中,獲取了知道子View測量的MeasureSpec
後,接下來就要呼叫child.measure()
方法,並把獲取到的childMeasureSpec
傳入。這時便又會呼叫onMeasure()
方法,若此時的子View為ViewGroup
的子類,便會呼叫相應容器類的onMeasure()
方法,其他容器View
的onMeasure()
方法與FrameLayout
的onMeasure()
方法執行過程相似。
下面會我們回到FrameLayout
的onMeasure()
方法,當遞迴地執行完所有子View
的測量工作後,會呼叫resolveSizeAndState()
方法來根據之前的測量結果確定最終對FrameLayout
的測量結果並儲存起來。View
類的resolveSizeAndState()
方法的原始碼如下:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
// 父View給定的最大尺寸小於完全顯示內容所需尺寸
// 則在測量結果上加上MEASURED_STATE_TOO_SMALL
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
// 若specMode為EXACTLY,則不考慮size,result直接賦值為specSize
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
對於普通View,會呼叫View類的onMeasure()
方法來進行實際的測量工作,該方法的原始碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
對於普通View(非ViewgGroup)來說,只需完成自身的測量工作即可。以上程式碼中通過setMeasuredDimension()
方法設定測量的結果,具體來說是以getDefaultSize()
方法的返回值來作為測量結果。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;
}
由以上程式碼我們可以看到,View的getDefaultSize()
方法對於AT_MOST
和EXACTLY
這兩種情況都返回了SpecSize
作為result。所以若我們的自定義View直接繼承了View類,我們就要自己對wrap_content
(對應了AT_MOST)這種情況進行處理,否則對自定義View指定wrap_content
就和match_parent
效果一樣了。
layout階段
layout
階段的基本思想也是由根View
開始,遞迴地完成整個控制元件樹的佈局(layout)工作。
View.layout()
我們把對decorView
的layout()
方法的呼叫作為佈局整個控制元件樹的起點,實際上呼叫的是View
類的layout()
方法,原始碼如下:
public void layout(int l, int t, int r, int b) {
// l為本View左邊緣與父View左邊緣的距離
// t為本View上邊緣與父View上邊緣的距離
// r為本View右邊緣與父View左邊緣的距離
// b為本View下邊緣與父View上邊緣的距離
. . .
boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
. . .
}
. . .
}
這個方法會呼叫setFrame()
方法來設定View的mLeft、mTop、mRight
和mBottom
四個引數,這四個引數描述了View相對其父View的位置(分別賦值為l, t, r, b),在setFrame()方法中會判斷View的位置是否發生了改變,若發生了改變,則需要對子View進行重新佈局,對子View的區域性是通過onLayout()方法實現了。由於普通View( 非ViewGroup)不含子View,所以View類的onLayout()方法為空。因此接下來,我們看看ViewGroup類的onLayout()方法的實現。
ViewGroup.onLayout()
實際上ViewGroup
類的onLayout()
方法是abstract
,這是因為不同的佈局管理器有著不同的佈局方式。
這裡我們以decorView
,也就是FrameLayout
的onLayout()
方法為例,分析ViewGroup
的佈局過程:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
final int count = getChildCount();
final int parentLeft = getPaddingLeftWithForeground();
final int parentRight = right - left - getPaddingRightWithForeground();
final int parentTop = getPaddingTopWithForeground();
final int parentBottom = bottom - top - getPaddingBottomWithForeground();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
int childLeft;
int childTop;
int gravity = lp.gravity;
if (gravity == -1) {
gravity = DEFAULT_CHILD_GRAVITY;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT:
default:
childLeft = parentLeft + lp.leftMargin;
}
switch (verticalGravity) {
case Gravity.TOP:
childTop = parentTop + lp.topMargin;
break;
case Gravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2 +
lp.topMargin - lp.bottomMargin;
break;
case Gravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
在上面的方法中,parentLeft
表示當前View
為其子View
顯示區域指定的一個左邊界,也就是子View
顯示區域的左邊緣到父View
的左邊緣的距離,parentRight、parentTop、parentBottom
的含義同理。確定了子View
的顯示區域後,接下來,用一個for
迴圈來完成子View
的佈局。
在確保子View
的可見性不為GONE的情況下才會對其進行佈局。首先會獲取子View
的LayoutParams
、layoutDirection
等一系列引數。上面程式碼中的childLeft
代表了最終子View的左邊緣距父View
左邊緣的距離,childTop
代表了子View
的上邊緣距父View
的上邊緣的距離。會根據子View
的layout_gravity
的取值對childLeft
和childTop
做出不同的調整。最後會呼叫child.layout()
方法對子View
的位置引數進行設定,這時便轉到了View.layout()
方法的呼叫,若子View
是容器View
,則會遞迴地對其子View進行佈局。
到這裡,layout
階段的大致流程我們就分析完了,這個階段主要就是根據上一階段得到的View
的測量寬高來確定View
的最終顯示位置。顯然,經過了measure
階段和layout
階段,我們已經確定好了View
的大小和位置,那麼接下來就可以開始繪製View
了。
draw階段
對於本階段的分析,我們以decorView.draw()
作為分析的起點,也就是View.draw()
方法,它的原始碼如下:
public void draw(Canvas canvas) {
. . .
// 繪製背景,只有dirtyOpaque為false時才進行繪製,下同
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
. . .
// 繪製自身內容
if (!dirtyOpaque) onDraw(canvas);
// 繪製子View
dispatchDraw(canvas);
. . .
// 繪製滾動條等
onDrawForeground(canvas);
}
簡單起見,在上面的程式碼中我們省略了實現滑動時漸變邊框效果相關的邏輯。實際上,View
類的onDraw()
方法為空,因為每個View繪製自身的方式都不盡相同,對於decorView
來說,由於它是容器View
,所以它本身並沒有什麼要繪製的。dispatchDraw()
方法用於繪製子View
,顯然普通View
(非ViewGroup)並不能包含子View
,所以View
類中這個方法的實現為空。
ViewGroup類的dispatchDraw()
方法中會依次呼叫drawChild()
方法來繪製子View
,drawChild()
方法的原始碼如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
這個方法呼叫了View.draw(Canvas, ViewGroup,long)
方法來對子View
進行繪製。在draw(Canvas, ViewGroup, long)
方法中,首先對canvas
進行了一系列變換,以變換到將要被繪製的View
的座標系下。完成對canvas
的變換後,便會呼叫View.draw(Canvas)
方法進行實際的繪製工作,此時傳入的canvas
為經過變換的,在將被繪製View的座標系下的canvas
。
進入到View.draw(Canvas)
方法後,會向之前介紹的一樣,執行以下幾步:
- 繪製背景;
- 通過
onDraw()
繪製自身內容; - 通過
dispatchDraw()
繪製子View
; - 繪製滾動條
至此,整個View
的繪製流程我們就分析完了。若文中有敘述不清晰或是不準確的地方,希望大家能夠指出,謝謝大家:)
這篇講解View的繪製比較全面,解決了很多疑惑,如果想仔細研究View
的measure()、layout()、draw()
,可以看這篇:http://www.cnblogs.com/jycboy/p/6066654.html。這兩篇看完就會對view
的繪製流程很清楚了。
相關文章
- Android原始碼分析之View繪製流程Android原始碼View
- View的繪製二:View的繪製流程View
- Android進階(五)View繪製流程AndroidView
- 探究Android View 繪製流程,Canvas 的由來AndroidViewCanvas
- Android View繪製原理:繪製流程排程、測算等AndroidView
- 探究 Android View 繪製流程,Activity 的 View 如何展示到螢幕AndroidView
- View 繪製流程分析View
- Android自定義View之(一)View繪製流程詳解——向原始碼要答案AndroidView原始碼
- Android View繪製流程看這篇就夠了AndroidView
- Android View的繪製過程AndroidView
- View的繪製-measure流程詳解View
- Android系統原始碼分析--View繪製流程之-inflateAndroid原始碼View
- Android系統原始碼分析–View繪製流程之-setContentViewAndroid原始碼View
- Android系統原始碼分析--View繪製流程之-setContentViewAndroid原始碼View
- View繪製流程原始碼分析View原始碼
- 深入淺出Android系列之從ViewToBitmap延伸到View的繪製全過程AndroidView
- Android高階進階之路【一】Android中View繪製流程淺析AndroidView
- Android自定義View之Paint繪製文字和線AndroidViewAI
- 【Android進階】RecyclerView之繪製流程(三)AndroidView
- Android View 原始碼解析(三) – View的繪製過程AndroidView原始碼
- View繪製01-Android渲染系統中的ViewViewAndroid
- 深入理解Android 之 Activity啟動流程(Android 10)Android
- 每日一問:簡述 View 的繪製流程View
- Android View繪製原始碼分析 MeasureAndroidView原始碼
- 2018.03.15、View 繪製流程學習 筆記View筆記
- Android UI繪製流程及原理AndroidUI
- 從繪製時機深入淺出View.postView
- View 的繪製過程View
- Android自定義View之Window、ViewRootImpl和View的三大流程AndroidView
- 基於原始碼分析 Android View 繪製機制原始碼AndroidView
- Flutter之UI繪製流程二FlutterUI
- Android自定義view-自繪ViewAndroidView
- View繪製——畫多大?View
- View繪製——畫在哪?View
- 【Android自定義View】繪圖之文字篇(三)AndroidView繪圖
- 【Android自定義View】繪圖之Path篇(二)AndroidView繪圖
- 靈魂畫師,Android繪製流程——Android高階UIAndroidUI
- 繪製流程
- View的繪製三:UI繪製的三大步驟ViewUI