AI智能
改变未来

UI绘制流程源码分析


本篇内容:

  • 布局文件加载流程分析。
  • 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的话不执行这一步),绘制其他。

赞(0) 打赏
未经允许不得转载:爱站程序员基地 » UI绘制流程源码分析