前言
Android的view繪制流程是一個比較重要的問題,理解好view的繪制流程,對平時開發(fā)工作非常有幫助,同時也是面試中常問的問題。
本文嘗試從源碼角度,結合其他人的博客,描述一下我對view繪制的一些理解
概述

每個Activity都有一個window接口實現(xiàn)PhoneWindow,用于承載用戶界面。contentView就是R.id.content,setContentView()就是設置它的子View。
Window
窗口是圖像顯示的一個容器,獨占一個surface實例的顯示區(qū)域,surface由WindowManagerService分配,看一看成一張畫布,由canvas或者openGL在上面進行繪制后,通過SurfaceFlinger將多塊surface按一定順序排列混合好,輸出到FrameBuffer中,得以顯示。
android.view.Window包含三個核心組件:
- WindowManager.LayoutParams:布局參數(shù)
- callback:回調,一般在Activity中實現(xiàn)
- ViewTree:窗口所承載的控件樹
PhoneWindow
它是Window接口唯一的具體實現(xiàn)。setContentView()方法設置Activity時,實際上完成了PhoneWindow的ViewTree的設置;而requestWindowFeature()用來定制Activity關聯(lián)的PhoneWindow的外觀,實際上也是將外觀特性參數(shù)存儲到了PhoneWindow的mFeatures中,在窗口繪制階段生成外觀模板時,根據(jù)mFeatures生成特定外觀。
setContentView()
此方法完成了Activity的contentView創(chuàng)建,但是并沒有進行繪制。自定義activity繼承自Activity和AppCompatActivity時,此方法的執(zhí)行邏輯不一樣。
- 先說Activity:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
getWindow()返回Activity關聯(lián)的PhoneWindow:
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
// mContentParent即為上面提到的ContentView的父容器,若為空則調用installDecor()生成
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// 調用mLayoutInflater.inflate()方法來填充布局
// 填充布局也就是把我們設置的ContentView加入到mContentParent中
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
// cb即為該Window所關聯(lián)的Activity
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
// 調用onContentChanged()回調方法通知Activity窗口內容發(fā)生了改變
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
- AppCompatActivity:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
getDelegate()返回繼承自AppCompatDelegate的代理類,根據(jù)不同api的level調用不同類。但是無論api是多少,這里的邏輯都是一樣的。
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
邏輯比較簡單,不多說了。
LayoutInflater.inflate()
setContentView()調用了LayoutInflater.inflate()來進行資源文件解析。
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();
}
}
無論是Activity還是AppCompatActivity都把ContentView作為root參數(shù)傳進去,最終調用inflate(XmlPullParser, viewGroup, boolean)填充布局。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
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參數(shù))不可為空且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 is the root view that was found in the xml
// temp為xml布局文件的根View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
...
// 獲取父容器的布局參數(shù)(LayoutParams)
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
// 若attachToRoot參數(shù)為false,則我們只會將父容器的布局參數(shù)設置給根View
temp.setLayoutParams(params);
}
}
...
// Inflate all children under temp against its context.
// 遞歸加載根View的所有子View
rInflateChildren(parser, temp, attrs, true);
...
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
// 若父容器不為空且attachToRoot為true,則將父容器作為根View的父View包裹上來
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
// 若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;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
// 不是開始標記則繼續(xù)下一次迭代
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
// 對一些特殊標記做單獨處理
if (TAG_REQUEST_FOCUS.equals(name)) {
pendingRequestFocus = true;
consumeChildElements(parser);
} 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 (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
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);
}
View的繪制
ViewRoot
View的繪制是由ViewRootImpl負責的。
每個應用窗口window的decorView都有一個ViewRootImpl與之關聯(lián),他們之間由WindowManager(確切來說WindowManger接口的實現(xiàn)類WindowManagerImpl,繼而調用WindowManagerGlobal類)來維護關系。當ActivityThread調用handleResumeActivity()時,相當于調用了activity的onResume(),decorView與ViewRootImpl建立聯(lián)系:WindowManager.addView--WindowManagerImpl.addView--WindowManagerGlobal.addView:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
View的繪制是由ViewRootImpl完成,setView內部會調用requestLayout()完成異步刷新請求。
public void setView(View view, WindowManager.LayoutParams attrs,
View panelParentView) {
...
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
...
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
//View繪制入口
scheduleTraversals();
}
}
具體細節(jié)需要了解Activity啟動流程。
View繪制起點
scheduleTraversals()方法來調度一次完成的繪制流程,向主線程發(fā)送一個遍歷消息,最終調用ViewRootImpl的performTraversals()調用:
private void performTraversals() {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
...
}
- measure: 判斷是否需要重新計算View的大小,需要的話則計算;
- layout: 判斷是否需要重新計算View的位置,需要的話則計算;
- draw: 判斷是否需要重新繪制View,需要的話則重繪制。
measure階段
計算每個view需要多大尺寸。
performTraversals()中通過measureHierarchy(),計算出根view的measureSpec,隨后調用performMeasure()、onMeasure()計算各層次veiw的大小。
// 傳入的desiredWindowXxx為窗口尺寸
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res,
final int desiredWindowWidth, final int desiredWindowHeight) {
int childWidthMeasureSpec;
int childHeightMeasureSpec;
boolean windowSizeMayChange = false;
...
boolean goodMeasure = false;
...
if (!goodMeasure) {
//獲取根的measureSpec信息,約束了decorView的寬高
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
windowSizeMayChange = true;
}
}
...
return windowSizeMayChange;
}
最終,獲取到viewRoot的width黑height的MeasureSpec后,傳給performMeasure()。spectMode為EXACTLY,spectSize為window尺寸。
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
mView即為decorView,執(zhí)行View.measure()這個不可繼承的final方法。
/**
* 調用這個方法來算出一個View應該為多大。參數(shù)為父View對其寬高的約束信息。
* 實際的測量工作在onMeasure()方法中進行
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 判斷是否需要重新計算measureSpec
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int oWidth = insets.left + insets.right;
int oHeight = insets.top + insets.bottom;
widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth);
heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
}
// Suppress sign extension for the low bytes
long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
// 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT標記,則強制重新布局
// 比如調用View.requestLayout()會在mPrivateFlags中加入此標記
final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
// Optimize layout by avoiding an extra EXACTLY pass when the view is
// already measured as the correct size. In API 23 and below, this
// extra pass is required to make LinearLayout re-distribute weight.
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) {
// first clears the measured dimension flag
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
// 先嘗試從緩從中獲取,若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);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} 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);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// flag not set, setMeasuredDimension() was not invoked, we raise
// an exception to warn the developer
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("View with id " + getId() + ": "
+ getClass().getName() + "#onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
forceLayout為true,表示強制重新布局,可以通過View.requestLayout()來實現(xiàn);
-
needsLayout為true,這需要specChanged為true(表示本次傳入的MeasureSpec與上次傳入的不同),并且以下三個條件之一成立:
- sAlwaysRemeasureExactly為true: 該變量默認為false;
- isSpecExactly為false: 若父View對子View提出了精確的寬高約束,則該變量為true,否則為false;
- matchesSpecSize為false: 表示父View的寬高尺寸要求與上次測量的結果不同。
對于decorView來說,實際執(zhí)行測量工作的是FrameLayout的onMeasure()方法。View.onMeasure():
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
而FrameLayout.onMeasure():
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
//每一個子view最大寬度和最大高度
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());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
// Account for padding too
// 最大寬度、高度加上父view和前景區(qū)域的padding
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));
count = mMatchParentChildren.size();
if (count > 1) {
for (int i = 0; i < count; i++) {
final View child = mMatchParentChildren.get(i);
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec;
if (lp.width == LayoutParams.MATCH_PARENT) {
final int width = Math.max(0, getMeasuredWidth()
- getPaddingLeftWithForeground() - getPaddingRightWithForeground()
- lp.leftMargin - lp.rightMargin);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
width, MeasureSpec.EXACTLY);
} else {
childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
lp.leftMargin + lp.rightMargin,
lp.width);
}
final int childHeightMeasureSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
final int height = Math.max(0, getMeasuredHeight()
- getPaddingTopWithForeground() - getPaddingBottomWithForeground()
- lp.topMargin - lp.bottomMargin);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
height, MeasureSpec.EXACTLY);
} else {
childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
lp.topMargin + lp.bottomMargin,
lp.height);
}
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
FrameLayout是ViewGroup子類,而ViewGroup中有一個View[]類型成員變量mChildren,代表當前viewGroup的所有子view的集合,該變量為private,但是可以通過publish的getChildAt(int position)獲取到View[position]的子view,以及getChildCount()獲取所有子view數(shù)量。
- 通過getChildAt()獲取到每一個子view,隨后measureChildWithMargins()對每一個子view進行測量,計算出所有子view中,最大寬度和最大高度,加上父View的padding和前景區(qū)域的padding,然后會檢查是否設置了最小寬高,并與其比較,將兩者中較大的設為最終的最大寬高。最后,若設置了前景圖像,我們還要檢查前景圖像的最小寬高。
- 得到了maxHeight和maxWidth的最終值,表示當前容器View用這個尺寸就能夠正常顯示其所有子View(同時考慮了padding和margin)。而后我們需要調用resolveSizeAndState()方法來結合傳來的MeasureSpec來獲取最終的測量寬高,并保存到mMeasuredWidth與mMeasuredHeight成員變量中。
- 我們可以看到,容器View通過measureChildWithMargins()方法對所有子View進行測量后,才能得到自身的測量結果。也就是說,對于ViewGroup及其子類來說,要先完成子View的測量,再進行自身的測量(考慮進padding等)。
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);
}
最終,子view的測量時通過子view自身child.measure()完成的。子view測量需要自身的measureSpec,這個就需要父view的measureSpec和子view自身的layoutParams參數(shù)決定,子view的layoutParams代表著子view自身期待的大小。getChildMeasureSpec():
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);
}
首先,根據(jù)父view的measureSpec算出specMode和specSize,在計算出父view留給子view的最大可用空間size。
- SpecMode為EXACTLY,表示父View對子View指定了確切的寬高限制,此時子View的LayoutParams:
- childDimension為具體大?。鹤觱iew的resultSize為childDimension,即子View在LayoutParams指定的具體大小值;SpecMode為EXACTLY,即這種情況下若該子View為容器View,它也有能力給其子View指定確切的寬高限制(子View只能在這個寬高范圍內),若為普通View,它的最終測量大小就為childDimension
- childDimension為match_parent:表示子View想和父View一樣大。SpecSize為size,即父View的剩余可用大小;SpecMode為EXACTLY
- childDimension為wrap_content:表示子View想自己決定自己的尺寸(根據(jù)其內容的大小動態(tài)決定)。這種情況下子View的確切測量大小只能在其本身的onMeasure()方法中計算得出,父View此時無從知曉。所以暫時將子View的SpecSize設為size(父View的剩余大小);令子View的SpecMode為AT_MOST,表示了若子View為ViewGroup,它沒有能力給其子View指定確切的寬高限制,畢竟它本身的測量寬高還懸而未定。
- SpecMode為AT_MOST:
- childDimension為具體大?。鹤覸iew的SpecSize為childDimension,SpecMode為EXACTLY
- childDimension為match_parent:表示子View想和父View一樣大,故令子View的SpecSize為size,但是由于父View本身的測量寬高還無從確定,所以只是暫時令子View的測量結果為父View目前的可用大小。這時令子View的SpecMode為AT_MOST。
- childDimension為wrap_content:表示子View想自己決定大?。ǜ鶕?jù)其內容動態(tài)確定)。然而這時父View還無法確定其自身的測量寬高,所以暫時令子View的SpecSize為size,SpecMode為AT_MOST。
結論:當子View的測量結果能夠確定時,子View的SpecMode就為EXACTLY;當子View的測量結果還不能確定(只是暫時設為某個值)時,子View的SpecMode為AT_MOST。
(可參考一張表格,自行谷歌)
子view層層遍歷,調用measure(),完成測量。當遞歸地執(zhí)行完所有子View的測量工作后,會調用resolveSizeAndState()方法來根據(jù)之前的測量結果確定最終對FrameLayout的測量結果并存儲起來。
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;
// 若specMode為EXACTLY,則不考慮size,result直接賦值為specSize
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
上面介紹的onMeasure()是FrameLayout的,而普通view的onMeasure():
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
普通View(非ViewgGroup)來說,只需完成自身的測量工作即可。
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效果一樣了。—— 自定義View的注意事項
但是如果是View的派生類,如TextView、Button、ImageView等,它們的onMeasure方法系統(tǒng)了都做了重寫,不會這么簡單直接拿 MeasureSpec 的size來當大小,而去會先去測量字符或者圖片的高度等,然后拿到View本身content這個高度(字符高度等),如果MeasureSpec是AT_MOST,而且View本身content的高度不超出MeasureSpec的size,那么可以直接用View本身content的高度(字符高度等),而不是像View.java 直接用MeasureSpec的size做為View的大小。
layout階段
DecorView繼承自FrameLayout,onLayout()會執(zhí)行super().onLayout()。FrameLayout的onLayout類似于onMeasure,getChildAt()獲得child,調用每個child的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);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
...
}
...
}
setFrame()方法四個參數(shù)描述了View相對其父View的位置,setFrame()方法中會判斷View的位置是否發(fā)生了改變,若發(fā)生了改變,則需要對子View進行重新布局,changed。
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;
// Remember our drawn bit
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;
int oldHeight = mBottom - mTop;
int newWidth = right - left;
int newHeight = bottom - top;
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
// Invalidate our old position
invalidate(sizeChanged);
...
}
return changed;
}
子view布局通過onLayout()實現(xiàn),普通View( 非ViewGroup)不含子View,所以View類的onLayout()方法為空。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
ViewGroup.layout()
@Override
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
LayoutTransition是用于處理ViewGroup增加和刪除子視圖的動畫效果,也就是說如果當前ViewGroup未添加LayoutTransition動畫,或者LayoutTransition動畫此刻并未運行,那么調用super.layout(l, t, r, b),繼而調用到ViewGroup中的onLayout,否則將mLayoutSuppressed設置為true,等待動畫完成時再調用requestLayout()
ViewGroup.onLayout()
ViewGroup類的onLayout()方法是abstract,不同的布局管理器有著不同的布局方式。
decorView,即FrameLayout的onLayout():
@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顯示區(qū)域的左邊緣到父View的左邊緣的距離,根據(jù)父view的padding值確定。
- 子View的可見性不為GONE才會進行布局
- childLeft代表了最終子View的左邊緣距父View左邊緣的距離,由parentLeft,即父view的padding,和子view的margin,以及view自身寬度等因素計算而成。不過不同的verticalGravity、absoluteGravity等形式,計算法師不同
- childTop同理。
- 最后會調用child.layout()方法對子View的位置參數(shù)進行設置,若子View是容器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);
}
省略了實現(xiàn)滑動時漸變邊框效果相關的邏輯
View類的onDraw()方法為空,因為每個View繪制自身的方式都不盡相同,對于decorView來說,由于它是容器View,所以它本身并沒有什么要繪制的。
-
dispatchDraw()方法用于繪制子View,顯然普通View(非ViewGroup)并不能包含子View,所以View類中這個方法的實現(xiàn)為空。ViewGroup類的dispatchDraw()方法中會依次調用drawChild()方法來繪制子View
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)方法后,會向之前介紹的一樣,執(zhí)行以下幾步:
- 繪制背景;
- 通過onDraw()繪制自身內容;
- 通過dispatchDraw()繪制子View;
- 繪制滾動條

參考:深入理解Android之View的繪制流程
???????????Android View的繪制流程