这篇文章主要讲解的是View的绘制流程以及源码分析,讲解这些的主要目的是为了能够在理解View的工作原理上更好的自定义View。
首先讲解一下布局文件是如何展现到屏幕上的。
一、布局文件是如何呈现在屏幕上的
我们从Activity的setContentView(R.layout.activity_main)入手了解UI绘制的起始过程。点进源码,我们会看到
Activity.java
public void setContentView(@LayoutRes int layoutResID) {
//调用的是PhoneWindow的setContentView
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
private Window mWindow;
public Window getWindow() {
return mWindow;
}
从源码我们可以看到,会调用getWindow,而返回的是一个Window的mWindow,我们再搜索源码,发现mWindow其实是一个PhoneWindow,那么,可以看出,activity的setContentView实际上最终调用的是PhoneWindow的setContentView,我们继续看深入,
PhoneWindow.java(这个文件位于系统源码中,不是在sdk源码中)
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) {
//初始化DecorView和mContentParent
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(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
我们可以可以看到如果mContentParent == null,则installDecor();我们首次加载肯定是为null的,那么我们继续点开查看:
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
//实例化DecorView
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
//初始化mContentParent
mContentParent = generateLayout(mDecor);
//省略部分代码
}
}
在此函数中,通过generateDecor实例化了DecorView,继续往下看:
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext().getResources());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
直接new了一个DecorView,那么DecorView是何物?通过继续点开源码其实DecorView就是一个FramenLayout,我们继续看installDecor(),如果mContentParent为空,则generateLayout(mDecor);
protected ViewGroup generateLayout(DecorView decor) {
//省略部分代码
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
layoutResource = R.layout.screen_swipe_dismiss;
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleIconsDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_title_icons;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
// System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
&& (features & (1 << FEATURE_ACTION_BAR)) == 0) {
// Special case for a window with only a progress bar (and title).
// XXX Need to have a no-title version of embedded windows.
layoutResource = R.layout.screen_progress;
// System.out.println("Progress!");
} else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
// Special case for a window with a custom title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogCustomTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else {
layoutResource = R.layout.screen_custom_title;
}
// XXX Remove this once action bar supports these features.
removeFeature(FEATURE_ACTION_BAR);
} else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
// If no other features and not embedded, only need a title.
// If the window is floating, we need a dialog layout
if (mIsFloating) {
TypedValue res = new TypedValue();
getContext().getTheme().resolveAttribute(
R.attr.dialogTitleDecorLayout, res, true);
layoutResource = res.resourceId;
} else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
layoutResource = a.getResourceId(
R.styleable.Window_windowActionBarFullscreenDecorLayout,
R.layout.screen_action_bar);
} else {
layoutResource = R.layout.screen_title;
}
// System.out.println("Title!");
} else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
layoutResource = R.layout.screen_simple_overlay_action_mode;
} else {
// Embedded, so no decoration is needed.
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
mDecor.startChanging();
//将布局文件id传递给DecorView
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
//省略部分代码
return contentParent;
}
主要逻辑是将布局文件的id传递给DecorView的onResourcesLoaded,在onresourcesLoaded中:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
mStackId = getStackId();
if (mBackdropFrameRenderer != null) {
loadBackgroundDrawablesIfNeeded();
mBackdropFrameRenderer.onResourcesLoaded(
this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
getCurrentColor(mNavigationColorViewState));
}
mDecorCaptionView = createDecorCaptionView(inflater);
final View root = inflater.inflate(layoutResource, null);
if (mDecorCaptionView != null) {
if (mDecorCaptionView.getParent() == null) {
addView(mDecorCaptionView,
new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mDecorCaptionView.addView(root,
new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
} else {
// Put it below the color views.
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
mContentRoot = (ViewGroup) root;
initializeElevation();
}
可以看出,是将这个View add进了DecorView,这个布局文件到底是什么,我们选择一个来看,
R.layout.screen_simple:
实际上就是一个LineaLayout,里面有个content,而且ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);,int ID_ANDROID_CONTENT = com.android.internal.R.id.content;,我们就可以知道这个mContentParent就是这个id为content的FrameLayout,我们再看PhoneWindow的setContentView,其中有 mLayoutInflater.inflate(layoutResID, mContentParent);,而且这个其实就是将我们的自己写的布局,放在content的里面
到此,我们可以就可以总结为下图:
调用过程:Activity setContentView->PhoneWindow setContentView->installDecor->generateLayout,层层深入,至此我们已经知道了一个activity的页面的布局结构是什么样子了,下面来看View的绘制流程
二、View的绘制流程
View的绘制入口是从ViewRootImpl的requestLayout方法开始的,经过measure,layout,draw最终将一个VIew绘制出来。measure用来View的测量,layout用来View的摆放,draw用来View的绘制,可以总结为下图
源码:
ViewRootImpl:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
//省略部分代码
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//省略部分代码
}
}
还可以看到mTraversalRunnable其实是个Runnable子类
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
又异步执行了doTraversal()方法:
void doTraversal() {
//省略部分代码
performTraversals();
//省略部分代码
}
}
最终调用了performTraversals()方法,而在performTraversals()中,其实主要就干了三件事:
performTranversal(){
//省略部分代码
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//省略部分代码
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
//省略部分代码
performDraw();
//省略部分代码
}
执行了三个方法,我们着重分析一下performMeasure,这个是View的绘制中相对比较难理解的地方,我们来看perfromMeasure的源码:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
//调用了View的measure方法
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
View.java:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//省略部分代码
//最终调用了onMeasure
onMeasure(widthMeasureSpec, heightMeasureSpec);
//省略部分代码
}
其实最终调用了我们熟悉的onMeasure方法,我们还可以知道View的子类分为ViewGroup(比如FrameLayout)和单纯的View(TextView),那么onMeasure有什么不同的,我们先看看FrameLayout的源码:
FrameLayout.java:
@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;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//测量子View
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
//省略部分代码
}
}
//测量自己
// 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());
}
//设置当前FrameLayout自身宽高
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
//省略部分代码
}
我们只看关键部分,在FrameLayout的onMeasure中,通过for循环遍历每个子View,并调用measureChildWithMargins,我们从名称就可以猜出来,其实是测量子View的意思,测量完子View,再测量自己,最后通过setMeasuredDimension设置自己的最终尺寸,我们可以总结为下图:
我们再来深究测量子View的方法:measureChildWithMargins,
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//获取子View的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//生成子View 宽的MeasureSpec
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);
//进行子View的测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
代码很简单,具体的解释也在注释里面说了,这里面提到一个MeasureSpec的概念,这个需要详细说一下
关于MeasureSpec
MeasureSpec你可以理解为一种“测量规格”,在测量过程中,系统会将View的LayoutParams以及父容器的测量规格转换成此View的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高,这个宽高是测量宽高,并不一定是最终的宽高
MeasureSpec是一个int值,32位,前两位是SpecMode,后面的30位是SpecSize:
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
MeasureSpec通过将SpecMode和SpecSize打包成一个int值避免过多的创建对象,节省内存。
SpecMode有三种:
EXECTLY:父容器已经测量出这个View的精确值,此时这个View的值就是SpecSize所指定的值,它对应于具体的数值以及LayoutParams中的match_parent
AT_MOST:父容器指定了一个可以使用的大小,也就是SpecSize,View的大小不能大于这个值,具体多大,就得看具体的实现,对应于LayoutParams中的wrap_parent
UNSPECIFIED:父容器不对Viwe有任何显示,要多大有多大,一般用于系统内部,或者ScrollView等
我们再来分析一下FrameLayout中的getChildMeasureSpec,看看系统是如何通过父View的MeasureSpec以及子View的LayoutParams来生成子View的MeasureSpec的:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//获取父View的specMode和specSize
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
//父View是EXACTLY的specMode
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {//如果子View设置的是具体的数值,不是match_parent或者wrap_content
//子View的尺寸就是设置的值
resultSize = childDimension;
//子View的SpecMode就是MeasureSpec.EXACTLY
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {//子View设置的是match_parent
// Child wants to be our size. So be it.
//子View的尺寸就是size
resultSize = size;
//specMode就是EXACTLY
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
//子View设置的是wrap_content,则自View的可用大小是父View中的size,模式是AT_MOST
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;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
以上代码看着挺多,实则不难,实际上就是根据父View的MeasureSpec再结合子View设置的尺寸值,来决定子View的MeasureSpec,最终,子View再根据这个MeasureSpec来进行测量
到这里基本上就把View的测量介绍完了,还剩下onLayout和onDraw,这两个在View的绘制流程中比较简单,就不再赘余,最后放上简单的自动换行自定义ViewGroup:
public class AutoWrapLayout extends ViewGroup {
private int horizontalSpace = 20;
private int verticalSpace = 20;
public AutoWrapLayout(Context context) {
super(context);
}
public AutoWrapLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AutoWrapLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = 0;
int resultHeight = 0;
//measure children
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams lp = child.getLayoutParams();
int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight(), lp.width);
int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom(), lp.height);
child.measure(childWidthSpec, childHeightSpec);
}
//measure self
switch (widthMode) {
case MeasureSpec.EXACTLY:
resultWidth = widthSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
resultWidth = getPaddingLeft() + getPaddingRight();
break;
}
switch (heightMode) {
case MeasureSpec.EXACTLY:
resultHeight = heightSize;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.UNSPECIFIED:
resultHeight = getPaddingTop() + getPaddingBottom();
break;
}
setMeasuredDimension(resultWidth, resultHeight);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft();
int top = getPaddingTop();
int right = 0;
int bottom = 0;
int maxHeightPerRow = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getMeasuredWidth() > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
throw new IllegalArgumentException("child view is larger than parent view");
}
right = left + child.getMeasuredWidth();
if (right > getMeasuredWidth() - getPaddingRight()) {
left = getPaddingLeft();
top += maxHeightPerRow + verticalSpace;
maxHeightPerRow = 0;
right = left + child.getMeasuredWidth();
}
bottom = top + child.getMeasuredHeight();
maxHeightPerRow = Math.max(maxHeightPerRow, child.getMeasuredHeight());
child.layout(left, top, right, bottom);
left = right + horizontalSpace;
}
}
}