Android高级UI系列(1)-- setContentView()详解

想要学好自定义View, 就要知道Android是如何将View显示到屏幕上的, 我们都知道我们创建Activity,设置setContentView()后, 我们的布局就显示在了我们的屏幕上, 今天我们就来看看setContentView()到底做了什么神奇的事情。

在开始之前, 我们要带着疑问去看源码。

疑问:

  • setContentView()到底做了什么, 为什么调用后就可以先出我们想要的布局页面?
  • PhoneWindow是什么东西?Window和它是什么关系?
  • DecorView是干什么用的?和我们的布局有什么关系?
  • 为什么requesetFeature()要在setContentView()之前调用?

相信大家在做开发的时候都会遇到这些疑问。

Activity的setContentView

注: 我们先讲activity中的setContentView()

我们现在就点进setConentView()一探究竟。我们点进去一看

getWindow().setContentView(layoutResID);
initWindowDecorActionBar();

它调用的是getWindow()的setContentView(), 那这个Window是什么呢。我们可以看Window类的注释。这里解释下注释:Window就是显示View的顶层的窗口, 并包含一些行为的封装。 这个Window的实例要作为一个顶级View添加到WindowManager中, 它提供一些背景, 标题栏, 默认的key事件的一些处理。

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
    ...
}

Window的实现类只有一个PhoneWindow, 因此, 刚才的getWindow().setContentView(layoutResID)其实调用的是PhoneWindow的setContentView()。好,我们现在去PhoneWindow的setContentView()看看。

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.
        //首先他会对mContentParent判空
        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;
    }

上面的代码,我们一句一句看, 首先它会对mContentParent进行判空, 那这个mContentParent是什么呢? 它是一个ViewGroup, 也就是它是一个容器,那么通过它的方法名, 我们也可以猜到, 它是一个现实内容的容器, 也就是我们的Activity要显示的内容,就会包到这个容器中。我们可以看这个mContentParent的注释。 它说这个mContentParent可以是DeocrView或者是它的子类。

  // This is the view in which the window contents are placed. It is either
  // mDecor itself, or a child of mDecor where the contents go.
    ViewGroup mContentParent;

在PhoneWindow类中我们再看DecorView的注释。说这是window的顶层view,包含了window的装饰。那这个DecorView是什么呢, 我们点这个类一看它就是一个FrameLayout.

// This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

至此, 我们大体上可以知道我们的Activity显示的是什么了。首先最外面是一个PhoneWindow, PhoneWindow里面包含了一个DecorView。

好, 我们继续往下看PhoneWindow的setContentView()。我们会看到这么一句话

 mLayoutInflater.inflate(layoutResID, mContentParent);

上面的代码把我们的XML布局添加到了mContentParent的容器中。我们暂且先这样猜测一下。

那我们DecorView是如何初始化的呢?那我们要看它的installDecor()方法了。我们点击去看。代码有点长, 我先贴上来。

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

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());
                if (mDecorContentParent.getTitle() == null) {
                    mDecorContentParent.setWindowTitle(mTitle);
                }

                final int localFeatures = getLocalFeatures();
                for (int i = 0; i < FEATURE_MAX; i++) {
                    if ((localFeatures & (1 << i)) != 0) {
                        mDecorContentParent.initFeature(i);
                    }
                }

                mDecorContentParent.setUiOptions(mUiOptions);

                if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 ||
                        (mIconRes != 0 && !mDecorContentParent.hasIcon())) {
                    mDecorContentParent.setIcon(mIconRes);
                } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 &&
                        mIconRes == 0 && !mDecorContentParent.hasIcon()) {
                    mDecorContentParent.setIcon(
                            getContext().getPackageManager().getDefaultActivityIcon());
                    mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK;
                }
                if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 ||
                        (mLogoRes != 0 && !mDecorContentParent.hasLogo())) {
                    mDecorContentParent.setLogo(mLogoRes);
                }

                // Invalidate if the panel menu hasn't been created before this.
                // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
                // being called in the middle of onCreate or similar.
                // A pending invalidation will typically be resolved before the posted message
                // would run normally in order to satisfy instance state restoration.
                PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
                if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) {
                    invalidatePanelMenu(FEATURE_ACTION_BAR);
                }
            } else {
                mTitleView = (TextView) findViewById(R.id.title);
                if (mTitleView != null) {
                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                        final View titleContainer = findViewById(R.id.title_container);
                        if (titleContainer != null) {
                            titleContainer.setVisibility(View.GONE);
                        } else {
                            mTitleView.setVisibility(View.GONE);
                        }
                        mContentParent.setForeground(null);
                    } else {
                        mTitleView.setText(mTitle);
                    }
                }
            }

            if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) {
                mDecor.setBackgroundFallback(mBackgroundFallbackResource);
            }

            // Only inflate or create a new TransitionManager if the caller hasn't
            // already set a custom one.
            if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
                if (mTransitionManager == null) {
                    final int transitionRes = getWindowStyle().getResourceId(
                            R.styleable.Window_windowContentTransitionManager,
                            0);
                    if (transitionRes != 0) {
                        final TransitionInflater inflater = TransitionInflater.from(getContext());
                        mTransitionManager = inflater.inflateTransitionManager(transitionRes,
                                mContentParent);
                    } else {
                        mTransitionManager = new TransitionManager();
                    }
                }

                mEnterTransition = getTransition(mEnterTransition, null,
                        R.styleable.Window_windowEnterTransition);
                mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION,
                        R.styleable.Window_windowReturnTransition);
                mExitTransition = getTransition(mExitTransition, null,
                        R.styleable.Window_windowExitTransition);
                mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION,
                        R.styleable.Window_windowReenterTransition);
                mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null,
                        R.styleable.Window_windowSharedElementEnterTransition);
                mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition,
                        USE_DEFAULT_TRANSITION,
                        R.styleable.Window_windowSharedElementReturnTransition);
                mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null,
                        R.styleable.Window_windowSharedElementExitTransition);
                mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition,
                        USE_DEFAULT_TRANSITION,
                        R.styleable.Window_windowSharedElementReenterTransition);
                if (mAllowEnterTransitionOverlap == null) {
                    mAllowEnterTransitionOverlap = getWindowStyle().getBoolean(
                            R.styleable.Window_windowAllowEnterTransitionOverlap, true);
                }
                if (mAllowReturnTransitionOverlap == null) {
                    mAllowReturnTransitionOverlap = getWindowStyle().getBoolean(
                            R.styleable.Window_windowAllowReturnTransitionOverlap, true);
                }
                if (mBackgroundFadeDurationMillis < 0) {
                    mBackgroundFadeDurationMillis = getWindowStyle().getInteger(
                            R.styleable.Window_windowTransitionBackgroundFadeDuration,
                            DEFAULT_BACKGROUND_FADE_DURATION_MS);
                }
                if (mSharedElementsUseOverlay == null) {
                    mSharedElementsUseOverlay = getWindowStyle().getBoolean(
                            R.styleable.Window_windowSharedElementsUseOverlay, true);
                }
            }
        }
    }

开始解释: 首先当Decor为空的时候它先会generateDecor(-1)生成一个Decor, 点进去看一下。这个方法就是new 了一个DeocrView ,代码我就不贴上来了。我们继续往下看,它把Decor生成之后, 它会把它丢到generateLayout(mDecor)方法中, 并且返回值赋给我们的ContentParent。 在这个地方, 我们来看一下generateLayout()方法里面做了些什么事情。这个方法非常的长,我就不贴上来了,我们挑重点的说, 其他的自行查看源码。

首先, 它会去拿我们的WindowStyle, 我们在XML设置的一些window相关style属性, 就是在这里加进来的。我们往后看, 比如, 它会判断window是不是floating的, 像Dialog这类的就是floating的。就是浮着的类型。我们在往后走, 会有一个我们熟悉的windowNoTitle的style, 它会调用requestFeature()方法。还有好多判断,大家可以自行去看。

看到这里我们开头的一个问题到这里已经渐渐明朗了, 为什么requesetFeature()要在setContentView()之前调用? 因为在我们setContentView()的时候, 就要根据不同的feature, 去加载不同的布局文件。这个布局文件, 就是将要加载到我们的Decor里面的。代码好多 就不贴上来了。

 // Inflate the window decor.

        int layoutResource;
        int features = getLocalFeatures();
        ...
         if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
        ...
        }

我们就挑个简单的布局(R.layout.screen_simple)看一下。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

其中id为content的就是要显示我们真正布局的地方, 我们来验证下。继续看源码,它会调用

 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
  ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

这个就是加载我们的布局, 同时findViewById,找到了我们刚才看的framelayout, 我们最后可以发现它返回的就是这个这个framelayout.

return contentParent;

那么讲到这里, 也就是说我们的mContentParent它最终代表的就是我们的DecorView布局里面id为content的FrameLayout.而我们刚才说了在PhoneWindow的setContentView()中mContentParent会最终加载我们的布局。也就是说, 我们Activity的布局最终显示在id为content的FrameLayout下面的。

如果有同学看不明白, 我这里放一张图, 能更加清晰的理解。

《Android高级UI系列(1)-- setContentView()详解》 1_Activity加载UI-类图关系和视图结构.png

而我们开头的有些疑惑都可以解开了:

PhoneWindow是什么东西?Window和它是什么关系? 我们每个Activity有一个这样的window, 这个window就是用来显示我们的UI的。

DecorView是干什么用的?和我们的布局有什么关系? DecorView是我们Window顶层的一个view, 我们自己的布局是加载到DecorView中的id为content的FrameLayout中的。

讲到这里, 我们仅仅只是把DecorView的视图结构给分析了一把, 那么我们自己的布局到底是怎么从我们的XML生成并且添加到DecorView上面的呢?因此,我们就要来看下LayoutInflater, 它是怎么把XML添加到DecorView中的?

当然我们继续要带着疑问去看源码。

相信我们平时在写我们的布局的时候, 会用到我们的include标签, 会用到我们的merge标签。你们会不会有这样的疑问:

  • include标签为什么不能作为我们的XML的根节点吗?
  • merge标签为什么它要作为XML资源布局的根节点?

接下来我们就带着疑问去看一下源码。其实我们所有的疑惑都可以在源码中找到答案。所以在开发过程中,不能有一知半解, 一定要花时间去探究一下。

我们的PhoneWindow的setContentView()源码并没有分析完, 因此我们继续看, 这时候就要看它的inflate方法了。

  mLayoutInflater.inflate(layoutResID, mContentParent);

刚才我们猜测过, 它这里肯定是把我们XML解析成view添加到mContentParent中去, 我们点进去看一下,它调用的是mLayoutInflater.inflate(layoutResID, mContentParent,true)。

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
    
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

首先它会去哪一个XML的解析器, 这个不细说了不是重点,我们继续到下一个inflate方法去,

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 {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                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(parser.getPositionDescription()
                        + ": " + 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;
        }
    }

首先,回去拿attrs, xml中设置的一些属性, 往下走, 它会把我们这支的contentParent赋值给result, 它会去找根节点, 它会去判断是不是merge标签, 如果root为空, 或者attachToRoot我false, 就会抛异常,这也就是为什么merge要作为顶层标签。接着他会调用rInflate(parser, root, inflaterContext, attrs, false);这个放着先待会再讲。

当是一些常用的标签的时候, 它首先会final View temp = createViewFromTag(root, name, inflaterContext, attrs);去生成常用布局,假设我们这个标签是LinearLayout, 接着会调用params = root.generateLayoutParams(attrs); 也就是说, 我们XML中的那些layout属性,都是调用对应的父节点的root.generateLayoutParams去解析属性,生成layoutParams, 添加上去的。

当把我们的根节点解析完成后, 会去解析子节点

 // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);

这个方法还是调用了void rInflate(XmlPullParser parser, View parent, Context context,AttributeSet attrs, boolean finishInflate)方法。这个方法在是merge和不是merage的时候都会调用, 只是最后的一个参数传的不同, 为merge标签的时候,最后参数为false, 其他情况为true.这个方法内部也是遍历循环解析.在这个方法里面我们就能回答刚才提出的两个问题了。

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)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                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();
        }
    }

到这里如果还没有明白的同学, 可以结合下图,可以更加容易的理解整个流程。

《Android高级UI系列(1)-- setContentView()详解》 LayoutInflater.png

讲到这里我们小结一下:

每个Activity都有一个关联的Window对象, 用来描述应用程序窗口, 每一个窗口内部又包含一个DecorView对象, DecorView对象用来描述窗口的视图–xml布局。

AppCompatActivity的setContentView

上述是在activity下创建DecorView的过程。

接下来我们要加个餐。我们用AS开发并不是继承Activity, 而是继承AppCompatActivity。所以他的setContentView()是谷歌工程做过优化的, 我们点进去看一下。

    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

一看名字, 就知道这是个代理。它这个代理就是代理了一些专门用来做兼容的一些类。getDelegate()获取一个AppCompatDelegate类, 这是一个抽象类, 它有好多实现, 这个自己点进去看, 很容易看明白。

那我们这个setContentView()是在哪个类中实现的呢? 不卖关子了, 是AppCompatDelegateImplV9中实现的。我们就去这个类中看看。我们到这个类的setContentView()中去看

 @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

我们看第一行的方法。ensureSubDecor()里面显示调用了createSubDecor()这个里面会很熟悉。我们点进去看。这个方法和PhoneWindow的generateLayout()很像,你们发现了吗? 这个方法里, 它会去先获取一些AppCompatTheme属性,这里就能解释为什么我们在style里面不去使用AppCompatTheme的主题的话,程序就会闪退了。因为我们默认继承的是AppCompatActivity。它的setContentView()内部获取的主题是AppCompatTheme的。我们继续往下看,我们会看到这么一行代码。

 // Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();

这个类还是会去初始化decorView的, 我们点进去看它还是调用了installDecor()去生成DecorView。

我们上面讲过PhoneWindow的generateLayout()里面是根据不同的style去加载不同的布局, 这个createSubDecor()也不例外, 我们看它后面的代码, 也是根据获取的style去加载不同的布局。

我们挑一个简单的subDecor看看,R.layout.abc_screen_simple。

<android.support.v7.widget.FitWindowsLinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/action_bar_root"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:fitsSystemWindows="true">
+
+    <android.support.v7.widget.ViewStubCompat
+        android:id="@+id/action_mode_bar_stub"
+        android:inflatedId="@+id/action_mode_bar"
+        android:layout="@layout/abc_action_mode_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content" />
+
+    <include layout="@layout/abc_screen_content_include" />
+
+</android.support.v7.widget.FitWindowsLinearLayout>

里面的include就是一个framelayout

<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <android.support.v7.widget.ContentFrameLayout
            android:id="@id/action_bar_activity_content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:foregroundGravity="fill_horizontal|top"
            android:foreground="?android:attr/windowContentOverlay" />

</merge>

所以它整个结构就是一个linearlayout里面一个viewstub和一个framelayout。为什么要这个搞? 我们接着看,等下你就明白了。这就是一个subDecor。

我们继续看代码。首先会把id为action_bar_activity_content的这个Framelayout找到, 然后找到android.R.id.content, 然后谷歌工程师它把这个content的id抹掉, 赋值到这个Framelayout上去了。然后把这个subDecor设置到window的DecorView中的FrameLayout中去了。

谷歌工程师为了做兼容真的是花了很多心思。看到这里应该都明白了吧,谷歌工程师有套了一层decorView进去,并把content的id名重新赋值给新添加的subDecor中的FrameLayout作为我们要设置内容的布局。

谷歌这么做是为了不用改变我们自己的编程习惯,以前是叫content,现在还是叫content, 对开发者来说比较方便。

 final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);

        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        ...
         // Change our content FrameLayout to use the android.R.id.content id.
            // Useful for fragments.
        windowContentView.setId(View.NO_ID);
        contentView.setId(android.R.id.content);
        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);

如果还没有理解的同学,我再放上一张图用来更好的理解一下兼容的setContentView吧。

《Android高级UI系列(1)-- setContentView()详解》 AppCompatActivity的View布局结构.png

大家也可以自己用AS去自己看看view层级结构。

好了, 关于setContentView也已经讲述完毕了,下一节我们讲解DecorView是如何添加到Window上的,敬请期待。

参考

课后笔记

本文为猫舍原创作品,转载请注明原文出处: http://imlerry.com/2017/05/08/02.Android%E9%AB%98%E7%BA%A7UI%E7%B3%BB%E5%88%97%EF%BC%881%EF%BC%89-setContentView-%E8%AF%A6%E8%A7%A3/

    原文作者:lllerry
    原文地址: https://www.jianshu.com/p/3360116d0728
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞