你需要知道的inlcude标签属性

有了解可以通过<include>标签导入其他布局xml以实现服用的效果。但,一直不明白到底是怎么实现的。这篇就是对怎么实现的探索。

官网说明:

如果要使用 <include> 标记来替换布局属性,您必须同时替换 android:layout_heightandroid:layout_width 才能让其他布局属性生效。

为什么官网会这么说明,又是怎么实现呢?

入口

不管是从Activity还是Fragment设置布局,最后都会调用到LayoutInflater#inflate方法。

/**
 * Inflate a new view hierarchy from the specified xml resource. Throws
 * {@linkInflateException} if there is an error.
 *
 * @paramresourceID for an XML layout resource to load (e.g.,
 *<code>R.layout.main_page</code>)
 * @paramrootOptional view to be the parent of the generated hierarchy (if
 *<em>attachToRoot</em> is true), or else simply an object that
 *        provides a set of LayoutParams values for root of the returned
 *        hierarchy (if<em>attachToRoot</em> is false.)
 * @paramattachToRootWhether the inflated hierarchy should be attached to
 *        the root parameter? If false, root is only used to create the
 *        correct subclass of LayoutParams for the root view in the XML.
 * @return The root View of the inflated hierarchy. If root was supplied and
 *         attachToRoot is true, this is root; otherwise it is the root of
 *         the inflated XML file.
 */
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) + ")");
    }

    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

共有两处调用其他方法,分别是tryInflatePrecompiledinflate

tryInflatePrecompiled方法中变量mUseCompiledView为false时会return null。找到调用处只有一处可能为true,根据注释说明是用于单元测试,那么实际的方法就是inflate了。

inflate内部先判断name的值是否和TAG_NAME相同:

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 {
		...
}

找到TAG_MERGE的定义处,可以看到除了TAG_MERGE还有*TAG_INCLUDE且其中抛出的异常是说<merge />,大胆推测TAG_INCLUDE*就是我们要找的<include>的解析。

顺着可以找到用到*TAG_INCLUDE*的位置:rInflate方法。

...
else if (TAG_INCLUDE.equals(name)) {
    if (parser.getDepth() == 0) {
        throw new InflateException("<include /> cannot be the root element");
    }
    parseInclude(parser, context, parent, attrs);
}
...

抛出异常的情况也是说<include />标签,进一步证实,这就是我们要找的。那么parseInclude方法肯定就是具体的解析方法了。

...
final View precompiled = tryInflatePrecompiled(layout, context.getResources(),
            (ViewGroup) parent, /*attachToRoot=*/true);
        if (precompiled == null) {
            final XmlResourceParser childParser = context.getResources().getLayout(layout);

            try {
                final AttributeSet childAttrs = Xml.asAttributeSet(childParser);

                while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                    // Empty.
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(getParserStateDescription(context, childAttrs)
                            + ": No start tag found!");
                }

                final String childName = childParser.getName();

                if (TAG_MERGE.equals(childName)) {
                    // The <merge> tag doesn't support android:theme, so
                    // nothing special to do here.
                    rInflate(childParser, parent, context, childAttrs, false);
                } else {
                    final View view = createViewFromTag(parent, childName,
                        context, childAttrs, hasThemeOverride);
                    final ViewGroup group = (ViewGroup) parent;

                    final TypedArray a = context.obtainStyledAttributes(
                        attrs, R.styleable.Include);
                    final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                    final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                    a.recycle();

                    // We try to load the layout params set in the <include /> tag.
                    // If the parent can't generate layout params (ex. missing width
                    // or height for the framework ViewGroups, though this is not
                    // necessarily true of all ViewGroups) then we expect it to throw
                    // a runtime exception.
                    // We catch this exception and set localParams accordingly: true
                    // means we successfully loaded layout params from the <include>
                    // tag, false means we need to rely on the included layout params.
                    ViewGroup.LayoutParams params = null;
                    try {
                        params = group.generateLayoutParams(attrs);
                    } catch (RuntimeException e) {
                        // Ignore, just fail over to child attrs.
                    }
                    if (params == null) {
                        params = group.generateLayoutParams(childAttrs);
                    }
                    view.setLayoutParams(params);

                    // Inflate all children.
                    rInflateChildren(childParser, view, childAttrs, true);

                    if (id != View.NO_ID) {
                        view.setId(id);
                    }

                    switch (visibility) {
                        case 0:
                            view.setVisibility(View.VISIBLE);
                            break;
                        case 1:
                            view.setVisibility(View.INVISIBLE);
                            break;
                        case 2:
                            view.setVisibility(View.GONE);
                            break;
                    }

                    group.addView(view);
                }
            } finally {
                childParser.close();
            }
        }
...

方法的参数有四个,分别是解析相关的parserContext对象context,父View View还有熟悉的AttributeSet对象。只要有创建View的地方就有AttributeSet对象存在,其作用是只有xml布局中的属性。

再看方法内部,开始首先调用tryInflatePrecompiled,我们前面已经知道这是单元测试用到的,这里返回的肯定是null,再往下看。

if (TAG_MERGE.equals(childName)) {
		// The <merge> tag doesn't support android:theme, so
    // nothing special to do here.
rInflate(childParser, parent, context, childAttrs, false);
} else {
		...
}

这里还是*TAG_MERGE,不管它。*

// else分支
final View view = createViewFromTag(parent, childName,
    context, childAttrs, hasThemeOverride);
final ViewGroup group = (ViewGroup) parent;

final TypedArray a = context.obtainStyledAttributes(
    attrs, R.styleable.Include);
final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
final int visibility = a.getInt(R.styleable.Include_visibility, -1);
a.recycle();

// We try to load the layout params set in the <include /> tag.
// If the parent can't generate layout params (ex. missing width
// or height for the framework ViewGroups, though this is not
// necessarily true of all ViewGroups) then we expect it to throw
// a runtime exception.
// We catch this exception and set localParams accordingly: true
// means we successfully loaded layout params from the <include>
// tag, false means we need to rely on the included layout params.
ViewGroup.LayoutParams params = null;
try {
    params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
    params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);

// Inflate all children.
rInflateChildren(childParser, view, childAttrs, true);

if (id != View.NO_ID) {
    view.setId(id);
}

switch (visibility) {
    case 0:
        view.setVisibility(View.VISIBLE);
        break;
    case 1:
        view.setVisibility(View.INVISIBLE);
        break;
    case 2:
        view.setVisibility(View.GONE);
        break;
}

group.addView(view);

else分支上来就创建了个View,还把parent转成了ViewGroup,把它添加进去了,不用头想也知道这个就是include标签中的布局了。

我们看到有个AttributeSet参数childAttrs,传入的要么是include标签的要么是被include的布局的。手写个demo,在被加载布局的构造方法打个断点,看到attrs中的属性分明就是被include布局的。方法的参数attrs肯定就是include标签的属性了。

这里有个疑问,怎么实现include标签中有layout_widthlayout_height时,属性是inlcude标签上的呢?

属性无非就两种来源,不是在xml布局中的设置的就是创建好View后设置的。

// We try to load the layout params set in the <include /> tag.
// If the parent can't generate layout params (ex. missing width
// or height for the framework ViewGroups, though this is not
// necessarily true of all ViewGroups) then we expect it to throw
// a runtime exception.
// We catch this exception and set localParams accordingly: true
// means we successfully loaded layout params from the <include>
// tag, false means we need to rely on the included layout params.
ViewGroup.LayoutParams params = null;
try {
    params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}
if (params == null) {
    params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);

除了看到setLayoutParams,注释也说的很清楚了。肯定就是我们所说的“创建好View后设置”的方法了。

/**
 * Returns a new set of layout parameters based on the supplied attributes set.
 *
 * @paramattrsthe attributes to build the layout parameters from
 *
 * @return an instance of {@linkandroid.view.ViewGroup.LayoutParams} or one
 *         of its descendants
 */
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

两个方法都是new一个新的LayoutParams对象。子类重写方法,返回不同的类型。后面rInflateChildren则是加载include中的子Viewinclude无关了。