本篇内容:
- 布局文件加载流程分析。
- View的绘制源码分析。
布局文件加载流程:
我们从MainActivity讲起
从
setContentView(R.layout.activity_main)
进去
来到了Activity下的:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public Window getWindow() {
return mWindow;
}
getWindow()
返回了个Window对象,这个对象就是我们的初始窗体,比如有ActionBar的Window,有菜单的Window,ToastWindow,输入法Window等等各种各样的窗体,那究竟这个mWindow是怎么样的我们不过多追究
我们在Activity.java下找到了实例化的位置:
mWindow = new PhoneWindow(this, window, activityConfigCallback);
PhoneWinow.java需要下载源码才能找到。
public class PhoneWindow extends Window implements MenuBuilder.Callback {
因为mWindow是个PhoneWindow对象,所以上面其实调用的是PhoneWindow下的setContentView(layoutResID);
@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) {
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;
}
第七行installDecor();
往窗体添加DecorView
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
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 = generateLayout(mDecor);
...
第四行generateDecor(-1);
生成并返回了DecorView对象
DecorVIew其实没什么大不了的:
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
我们也多追究,我们只要明白它也就个帧布局,现在知道为什么我们创建的activity.xml根布局可以有layout_width这种参数了吧。因为它有父布局!就是DecorView。
然后mContentParent = generateLayout(mDecor);
生成并返回一个布局
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
...
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}
...
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
ProgressBar progress = getCircularProgressBar(false);
if (progress != null) {
progress.setIndeterminate(true);
}
}
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
registerSwipeCallbacks(contentParent);
}
// Remaining setup -- of background and title -- that only applies
// to top-level windows.
if (getContainer() == null) {
mDecor.setWindowBackground(mBackgroundDrawable);
final Drawable frame;
if (mFrameResource != 0) {
frame = getContext().getDrawable(mFrameResource);
} else {
frame = null;
}
mDecor.setWindowFrame(frame);
mDecor.setElevation(mElevation);
mDecor.setClipToOutline(mClipToOutline);
if (mTitle != null) {
setTitle(mTitle);
}
if (mTitleColor == 0) {
mTitleColor = mTextColor;
}
setTitleColor(mTitleColor);
}
mDecor.finishChanging();
return contentParent;
}
代码很多3百多行,我只贴一部分
第五行requestFeature(FEATURE_NO_TITLE);
看到没有,这也是为什么我们在java代码中设置Activity没actionbar什么的需要在setContentView()
之前设置的原因了,其余代码大都是对layout布局的设置我们略过。
第14行,mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
加载layoutResource,这个就是我们传进来的布局资源,
进去:
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
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();
}
这里就是把传进来的layoutResource比如R.layout.screen_action_bar、R.layout.screen_custom_title等基本布局)利用LayoutInflater渲染并加到DecorView下。
这里粘个AndroidSDK\platforms\android-29\data\res\layoutscreen_action_bar.xml
给看一下
<com.android.internal.widget.ActionBarOverlayLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/decor_content_parent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:splitMotionEvents="false"
android:theme="?attr/actionBarTheme">
<FrameLayout android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.android.internal.widget.ActionBarContainer
android:id="@+id/action_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
style="?attr/actionBarStyle"
android:transitionName="android:action_bar"
android:touchscreenBlocksFocus="true"
android:keyboardNavigationCluster="true"
android:gravity="top">
<com.android.internal.widget.ActionBarView
android:id="@+id/action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/actionBarStyle" />
<com.android.internal.widget.ActionBarContextView
android:id="@+id/action_context_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
style="?attr/actionModeStyle" />
</com.android.internal.widget.ActionBarContainer>
<com.android.internal.widget.ActionBarContainer android:id="@+id/split_action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/actionBarSplitStyle"
android:visibility="gone"
android:touchscreenBlocksFocus="true"
android:keyboardNavigationCluster="true"
android:gravity="center"/>
</com.android.internal.widget.ActionBarOverlayLayout>
接下来调用了:
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
这个ID_ANDROID_CONTENT前面有声明成com.android.internal.R.id.content;
对应我们上面加载进DecorView的布局下的id值,这里获取ViewGroup,在方法最后面返回。
返回到上层:
mContentParent = generateLayout(mDecor);
mContentParent 这个全局变量拿到了这个ViewGroup,然后我们再回到上一层,installDecor();
做完后有这个判断
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
我们不知道判断了上面,但这肯定时把我们传进来的布局渲染到了mContentParent下,也就是DecorView的布局下。
那我们的布局渲染进主界面就分析完成了。
接下来我们讲VIew的绘制流程
重要的三个执行流程:
View.java
measure:测量
layout:摆放(主要摆放的是子View)
draw:绘制
我们上面讲到mLayoutInflater.inflate(layoutResID, mContentParent);
把我们的布局渲染到了DecorView上,我们进去看看怎么做的吧:
点进去来带
LayoutInflater类下的
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
调用了inflate,由于LayoutInflater有几个重载但是最终都会调用下面的重载方法:
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 {
advanceToRootNode(parser);
final String name = parser.getName();
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) {
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
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 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)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
第27行final View temp = createViewFromTag(root, name, inflaterContext, attrs);
解析并获得View对象。怎么解析我们今天不讲了,我们只将绘制。
第52行
if (root != null && attachToRoot) {
root.addView(temp, params);
}
把View add到了root上
点进去会来到ViewGroup下:
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
接下来就是关键了
首先调用了View.java的requestLayout()
方法,这个方法就是把整个试图树重新进行测量,摆放。
public void () {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
mParent.requestLayout();
会一直递归调用父窗口的requestLayout,直到ViewRootImpl
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
主要就是,把mLayoutRequested 设置为true,设置回调(scheduleTraversals()
->doTraversal()
->performTraversals()
),然会就会导致onMeasure()
和onLayout()
被调用,如果在layout过程中发现l,t,r,b和以前不一样或者发现其他触发条件,就会触发一次invalidate()
,导致onDraw()
调用。
performTraversals()
里重要的是调用了
performeasure()
里面会调用View的measure()
方法performLayout()
里面会调用View的layout()
方法performDraw()
里面会调用View的draw()
方法
View绘制流程:
我们先看看
performeasure()
:
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);
}
}
调用measure()
方法参数是将要测量的控件的宽高的信息(包括specMode和specSize), measure()
作了写调整工作后调用了OnMeasure()
方法.并把宽高信息传了进去
我们看下View.java下的
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在OnMeasure中最终都会调用
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
可以看到这个方法把测量结果设置到全局变量
所以说我们可以重写OnMeasure()
达到自定义测量. 如果是View的话可以测量自己有多大,如果是ViewGroup的话就需要先测量每个子控件的大小再计算得出自己大小(measureChild()
, measureChildWidthMargins()
)
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的测量:
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);
}
我们发现这两个方法都调用了getChildMeasureSpec()
这个方法,里面就是用子控件和父控件的宽高信息进行对子控件的测量.所以我们开发时也经常用这个方法来测量子控件宽高.
其中有三种模式:
- UNSPECIFIED表示未定义,即父控件未做限制,可以为任何值,一般设置为0。
- EXACTLY表示实际值,即父容器已经指定了具体的值。
- AT_MOST表示父容器提供了最大值,但子控件可以选择自己的范围。
我们上面分析得出测量工作完成后,把结果赋值到了全局变量。
接下来看怎么layout的吧
从performLayout()
进去看:
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
mLayoutRequested = false;
mScrollMayChange = true;
mInLayout = true;
final View host = mView;
if (host == null) {
return;
}
if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
Log.v(mTag, "Laying out " + host + " to (" +
host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
try {
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
...
}
较多代码,我们看到主要调用了host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
host就是DecorView,这个方法传入了DecorView的位置和宽高,其实就是调用了View下的layout()方法
里面使用传入的参数和之前测量好的信息计算好初始位置信息。又会调用onLayout(changed, l, t, r, b);
方法,并传入位置信息,接下来就按需求摆放了。
draw:
源码调用步骤:ViewRootImpl.performDraw()
->ViewRootImpl .draw()
->ViewRootImpl .drawSoftware()
->View.draw()
然后就是绘制背景,绘制内容,绘制子View(当然如果是View的话不执行这一步),绘制其他。