从 setConentView 开始讲起 View 的加载

真正应了一句话,看了那么多文章,却还是自定义不好一个组件。网上好多文章,大多数都是文章都是讲了一下 View 和 ViewGroup 的源码,View 和 ViewGroup 是如何 measure、layout 和 draw 的,看完之后,一头懵逼,我们该如何自定义呢?还是无从下手,所以我想着尽可能的从另一个角度来写一下,一来彻底搞清楚自定义组件,二来如果能帮助到其他人,也是极好的(虽然基本不可能有人来这里)。

从 setContentView 说起

我们标准步骤,创建一个 XML 布局文件,然后在 Activity 的 onCreate 方法当中调用 setContentView 来为这个 Activity 设置一个视图,其参数可以是一个 View,也可以是一个指向布局文件的 ID。

    private Window mWindow;
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
            ...
            mWindow = new PhoneWindow(this, window);
            ...
    }
    public Window getWindow() {
        return mWindow;
    }

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
    public void setContentView(View view) {
        getWindow().setContentView(view);
        initWindowDecorActionBar();
    }
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        getWindow().setContentView(view, params);
        initWindowDecorActionBar();
    }

可以看到 getWindow() 返回了当前 Activity 的 PhoneWindow(Window 的子类) 对象,然后又调用了 PhoneWindow 对象的 setContentView 方法,看一下这个方法:

    ViewGroup mContentParent;

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ...
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ...
        } else {
            mContentParent.addView(view, params);
        }
        ...
    }
       @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ...
        }
        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            ...
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        ...
    }

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

看到 setContentView 都会先判断 mContentParent 是否为空,如果为空,则会调用 installDecor()

    private void installDecor() {
        ...
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
            ...
        }
    }
    protected ViewGroup generateLayout(DecorView decor) {
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }
        return contentParent;
    }

其实 installDecor 方法可以总结为:

ViewGroup mContentParent = (ViewGroup) findViewById(ID_ANDROID_CONTENT)

现在 mContentParent != null 了,接下来根据 setContentView 方法的参数会执行不同的方法:

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        ...
            mContentParent.addView(view, params);
        ...
    }
       @Override
    public void setContentView(int layoutResID) {
        ...
            mLayoutInflater.inflate(layoutResID, mContentParent);
        ...
    }

    @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }

inflate 干了什么?

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }


    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        ...
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

    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 {
                ...
                final String name = parser.getName();
                ...

                if (TAG_MERGE.equals(name)) {
                    ...

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        ...
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    ...
                    rInflateChildren(parser, temp, attrs, true);

                    ...
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                ...
            } catch (Exception e) {
                ...
            } finally {
                ...
            }

            return result;
        }
    }


    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                ...
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

归根结底,最后对调用 ViewGroup 的 addView 方法:

    public void addView(View child) {
        addView(child, -1);
    }

    public void addView(View child, int index) {
        ...
        addView(child, index, params);
    }

    public void addView(View child, int width, int height) {
        ...
        addView(child, -1, params);
    }

    public void addView(View child, LayoutParams params) {
        addView(child, -1, params);
    }

    public void addView(View child, int index, LayoutParams params) {
        ...
        requestLayout();
        invalidate(true);
        addViewInner(child, index, params, false);
    }

看 requestLayout 方法:

    protected ViewParent mParent;
    public void requestLayout() {
        ...
        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
        ...
    }

它调用了 ViewParent 对象的 requestLayout() 方法,ViewParent 是一个接口,实际上它调用的是 ViewParent 的视线里 ViewRootImpl 的 requestLayout():

    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

checkThread() 方法用来检测是否是 UI 线程:

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

我们在子线程更新 UI 的时候,异常就是从这里跑出去的。

看 scheduleTraversale() 方法:

    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

重点是 mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

doTraversal() 中重点来了:

    void doTraversal() {
            ...
            performTraversals();
            ...
        }
    }

在这个方法中,开始执行测量、布局、绘制:

    private void performTraversals() {
        ...
        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
            ...
            if (!mStopped || mReportNextDraw) {
                ...
                     // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    ...
                }
            }
        } else {
            ...
        }
        ...
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);
            ...
        }
        ...
        if (!cancelDraw && !newSurface) {
            ...
            performDraw();
        } else {
            ...
        }
        mIsInTraversal = false;
    }

至此,requestLayout 执行完毕,系统也将 View 测量、定位并且画出来了。

然后调用 invalidate 方法刷新界面。

至此,界面完全的呈现在我们面前~

Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1