本篇内容:
- 布局文件加载流程分析。
- 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);
@Overridepublic 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.ActionBarOverlayLayoutxmlns: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.ActionBarContainerandroid: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.ActionBarViewandroid:id=\"@+id/action_bar\"android:layout_width=\"match_parent\"android:layout_height=\"wrap_content\"style=\"?attr/actionBarStyle\" /><com.android.internal.widget.ActionBarContextViewandroid: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 xmlfinal 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 suppliedparams = 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下:
@Overridepublic 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 levelrequestLayout();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 hierarchyViewRootImpl 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
@Overridepublic 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的话不执行这一步),绘制其他。