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);

@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的话不执行这一步),绘制其他。

发布了16 篇原创文章 · 获赞 0 · 访问量 249

猜你喜欢

转载自blog.csdn.net/weixin_43860530/article/details/105494217