这儿接上一篇LayoutInflater源码剖析持续剖析。

rinflate源码解析

这儿详细理一理rinflate办法,作用便是找到传入的XmlPullParser当时层级一切的view并add到parent上:

    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }
    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        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)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } 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 (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }
        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

很明显rinflate是个递归办法,代码很简略,递归-判别类型决议是否持续递归-递归。

递归

咱们知道,递归最重要的便是完毕条件的选取,这儿的完毕条件有这么几个:

  1. type != XmlPullParser.END_TAG
  2. parser.getDepth() > depth
  3. type != XmlPullParser.END_DOCUMENT

其实1和3都是惯例的完毕条件,最重要的是2这个条件,这个完毕条件保证了当时循环只读取本层的view,咱们结合一个比如来看一下。
下面是一个很简略的XmlPullParser解析的比如:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
        <Button
            android:id="@+id/btn_1"
            android:layout_width="80dp"
            android:layout_height="45dp" />
    </LinearLayout>
    <Button
        android:id="@+id/btn_2"
        android:layout_width="match_parent"
        android:layout_height="60dp" />
</RelativeLayout>

解析代码如下:

    public void readMainXml() {
        //1. 拿到资源文件
        InputStream is = getResources().openRawResource(R.raw.activity_main);
        //2. 拿到解析器目标
        XmlPullParser parser = Xml.newPullParser();
        final int depth = parser.getDepth();
        try {
            //3. 初始化xp目标
            parser.setInput(is, "utf-8");
            //4.开端解析
            //获取当时节点的事情类型
            int type = parser.getEventType();
            while (((type = parser.next()) != XmlPullParser.END_TAG ||
                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                switch (type) {
                    case XmlPullParser.START_TAG:
                        int attrCount = parser.getAttributeCount();
                        LogUtil.d("depth:" + parser.getDepth() + " - " + parser.getName() + " 标签开端");
                        for (int i = 0; i < attrCount; i++) {
                            String attrName = parser.getAttributeName(i);
                            String attrValue = parser.getAttributeValue(i);
                            //layout_width : match_parent
                            LogUtil.d("depth:" + parser.getDepth() + " - " + parser.getName() + "特点: " + attrName + " : " + attrValue);
                        }
                        break;
                    case XmlPullParser.END_TAG:
                        LogUtil.d("depth:" + parser.getDepth() + " - " + parser.getName() + "标签完毕");
                        break;
                    default:
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
//    D: depth:1 - RelativeLayout 标签开端
//    D: depth:1 - RelativeLayout特点: layout_width : match_parent
//    D: depth:1 - RelativeLayout特点: layout_height : match_parent
//    D: depth:2 - LinearLayout 标签开端
//    D: depth:2 - LinearLayout特点: layout_width : wrap_content
//    D: depth:2 - LinearLayout特点: layout_height : wrap_content
//    D: depth:3 - Button 标签开端
//    D: depth:3 - Button特点: id : @+id/btn_1
//    D: depth:3 - Button特点: layout_width : 80dp
//    D: depth:3 - Button特点: layout_height : 45dp
//    D: depth:3 - Button标签完毕
//    D: depth:2 - LinearLayout标签完毕
//    D: depth:2 - Button 标签开端
//    D: depth:2 - Button特点: id : @+id/btn_2
//    D: depth:2 - Button特点: layout_width : match_parent
//    D: depth:2 - Button特点: layout_height : 60dp
//    D: depth:2 - Button标签完毕
//    D: depth:1 - RelativeLayout标签完毕

这儿展现一个简略的XmlPullParser的比如,能够看到RelativeLayout有两个子View,分别是LinearLayoutButton2,depth都是2,结合上面的rinflate的代码能够了解,在View的递归树上,XmlPullParser的depth保证了层级,只会处理当时层级的View。

类型判别

办法体中做了类型的判别,特别判别了几种类型如下:

TAG_REQUEST_FOCUS

非容器控件标签中放标签,表明将当时控件设为焦点,能够放到标签里边,多个EditText的时分运用标签首先取得焦点。

TAG_TAG

标签里边都能够放,类似于代码中运用View.setTag:

    private void parseViewTag(XmlPullParser parser, View view, AttributeSet attrs)
            throws XmlPullParserException, IOException {
        final Context context = view.getContext();
        final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewTag);
        final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
        final CharSequence value = ta.getText(R.styleable.ViewTag_value);
        view.setTag(key, value);
        ta.recycle();
        consumeChildElements(parser);
    }

根据id获取value,并把id作为key,设置parent的Tag。能够看下面这个比如:

    <EditText
        android:id="@+id/et_1"
        android:layout_width="match_parent"
        android:layout_height="50dp">
        <tag
            android:id="@+id/tag1"
            android:value="tag_value" />
    </EditText>

能够运用findViewById(R.id.et_1).getTag(R.id.tag1),得到tag_value值,留意不能够运用getTag(),有参数无参数获取的不是同一个特点。

TAG_MERGE:

这儿还对标签做了二次的判别,保证标签不会出现在非root元素的方位。 假如不是上述特别的标签,运用createViewFromTag加载出来view,并用当时的attrs加载成LayoutParams设置给当时View,持续向下递归的同时把view add到parent.

TAG_INCLUDE

<include>标签能够实现在一个layout中引证另一个layout的布局,这一般适合于界面布局复杂、不同界面有共用布局的APP中,比如一个APP的顶部布局、侧边栏布局、底部Tab栏布局、ListView和GridView每一项的布局等,将这些同一个APP中有多个界面用到的布局抽取出来再通过<include>标签引证,既能够降低layout的复杂度,又能够做到布局重用(布局有改动时只需要修改一个地方就能够了)。

这些类型之外就类似于之前剖析过的处理,先调用createViewFromTag办法创立View,设置attrs特点,再调用递归办法rInflateChildren把view的子View add到view上,然后添加到parent上,直到层级遍历完毕。

下面重点看parseInclude的源码剖析:

parseInclude

private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;
//-------------------------------------第1部分-------------------------------------//
        if (!(parent instanceof ViewGroup)) {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }
        // 假如有theme特点,从当时View的attrs里边检查是否有theme特点,假如有,就重新创立ContextThemeWrapper,
        // 用当时View的theme替换之前ContextThemeWrapper里边的theme
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);//InflateActivityMergeTheme
        final boolean hasThemeOverride = themeResId != 0;
        if (hasThemeOverride) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
        // 检查当时view的attrs里边是否有layout的id,也便是’@layout/xxxx‘,假如没有就返回0
        int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
        if (layout == 0) {
            //找不到先找这个layout特点的值’@layout/xxxx‘,看layout特点的string是否为空,假如是空就直接抛异常,不为空才去找layoutId
            final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
            if (value == null || value.length() <= 0) {
                throw new InflateException("You must specify a layout in the"
                    + " include tag: <include layout=\"@layout/layoutID\" />");
            }
            // 假如取不到,就测验去"?attr/"下面找对应的特点。
            layout = context.getResources().getIdentifier(
                value.substring(1), "attr", context.getPackageName());
        }
        // The layout might be referencing a theme attribute.
        if (mTempValue == null) {
            mTempValue = new TypedValue();
        }
        if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
            layout = mTempValue.resourceId;
        }
        if (layout == 0) {
            final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
            throw new InflateException("You must specify a valid layout "
                + "reference. The layout ID " + value + " is not valid.");
        }
//-------------------------------------第2部分-------------------------------------//
        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.
                }
                final String childName = childParser.getName();
                if (TAG_MERGE.equals(childName)) {
                    // 假如是merge标签,不支持特点的设置,留意此处直接把parent作为父布局传入,也便是加载出来的子View直接挂到parent上。
                    rInflate(childParser, parent, context, childAttrs, false);
                } else {
                    final View view = createViewFromTag(parent, childName,
                        context, childAttrs, hasThemeOverride);
                    final ViewGroup group = (ViewGroup) parent;
                    // 获取include设置的id和visible。也便是说假如include设置了id和visible,会运用include设置的这两个特点
                    // 真正view设置的id和visible会不起作用
                    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();
                    // 先测验运用<include >标签的特点去创立params,判别的标准是有没有width/height特点
                    // 假如没有则运用view的特点去创立params,然后调用view.setLayoutParams给View设置特点
                    // 换言之,假如<include>设置了width/height特点,会整体掩盖view的特点,反之则不会。
                    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);
                    // 假如<include>标签设置了id和visibility特点则一定会替换里边的id和visibility特点
                    // 换言之,<include>标签设置了id和visibility特点,里边View的id和visibility会不起作用。
                    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();
            }
        }
        LayoutInflater.consumeChildElements(parser);
    }

两个部分:

  1. 查找<include /> 标签是否有layout特点,并应用适合的theme特点
  2. 判别是否是<merge>,不同的办法加载对应的view,替换对应的特点
榜首部分:查找layout特点

<include />最重要的便是用来做layout的替换,所以有必要设置一个layout特点,没有设置layout特点的<include />是没有意义的,有两种办法去设置这个layout特点: 一种是直接设置:

    <include
        layout="@layout/include_test_viewgroup"/>

这种也是咱们最常用的办法,这种办法咱们称作

第二种办法是自定义一个reference,在attrs中定义,这样也能够用来实现重用,比如:

//attrs.xml
    <declare-styleable name="TestInclude">
        <attr name="theme_layout" format="reference" />
    </declare-styleable>
//style.xml
    <style name="InflateActivityTheme" parent="AppTheme">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/red</item>
        <item name="theme_layout">@layout/include_test_merge</item>
    </style>

然后在layout中运用:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:theme="@style/InflateActivityTheme">
    <include layout="?attr/theme_layout"/>
</RelativeLayout>

上面这种办法咱们称作,或者下面这种咱们称作

<include
        layout="?attr/theme_layout"
        android:theme="@style/InflateActivityTheme" />

按照这几种的介绍咱们来走一遍上面查找layout的代码:

        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        final boolean hasThemeOverride = themeResId != 0;
        if (hasThemeOverride) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();

这是办法的区别,办法阐明传过来的context就有theme,办法表明能从attrs中找到theme特点,所以hasThemeOverride=true,假如需要掩盖就用当时view的theme重新创立了ContextThemeWrapper。这两者有一即可。

       // 检查当时view的attrs里边是否有layout的id,也便是’@layout/xxxx‘,假如没有就返回0
       int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
       if (layout == 0) {
           //找不到先找这个layout特点的值’@layout/xxxx‘,看layout特点的string是否为空,假如是空就直接抛异常,不为空才去找layoutId
           final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
           if (value == null || value.length() <= 0) {
               throw new InflateException("You must specify a layout in the"
                   + " include tag: <include layout=\"@layout/layoutID\" />");
           }
           // 假如取不到,就测验去"?attr/"下面找对应的特点。
           layout = context.getResources().getIdentifier(
               value.substring(1), "attr", context.getPackageName());
       }

关于办法,代码里其实写清楚了,先找@layout/xxx这样的,假如找不到就到?attr/下面找。

第二部分:加载对应的View并替换

这段的代码其实看上面代码里的注释就好了,很明晰。加载替换的layout有两种情况:

  1. merge标签,咱们知道 merge标签用于降低View树的层次来优化Android的布局,所以merge标签并不是一层View结构,能够了解成一个占位,遇到merge标签就直接调用rinflate办法,找到一切的子view挂到parent上就好了,所以给设置什么特点,其实没什么作用。
  2. merge标签的其他ViewGroup,createViewFromTag加载进来对应的ViewGroup后
    2.1. 测验运用<include />的特点,假如标签没有设置width/height这样的根底特点就运用加载进来的layout的特点。
    2.2. <include />标签总是起作用的特点有两个,一个是id,一个是visibility,假如<include />设置了这两个特点,总是会替换加载的layout的对应特点

设置完上面的特点之后,持续调用rInflateChildren去递归加载完一切的子view

其实这个逻辑很像刚inflate刚开端履行时分的逻辑,能够回想一下之前的代码。

这儿有个小demo来看清这几个的区别:

#styles.xml
    <style name="InflateActivityTheme" parent="AppTheme">
        <item name="colorPrimary">@color/red</item>
        <item name="theme_layout">@layout/include_test_merge</item>
    </style>
    <style name="InflateActivityMergeTheme" parent="AppTheme">
        <item name="colorPrimary">@color/green</item>
    </style>
    <style name="InflateActivityIncludeTheme" parent="AppTheme">
        <item name="colorPrimary">@color/blue</item>
    </style>

总的布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?attr/colorPrimary"
    android:theme="@style/InflateActivityTheme">
    <include
        layout="?attr/theme_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:theme="@style/InflateActivityMergeTheme" />
    <include
        layout="@layout/include_test_viewgroup"
        android:id="@+id/include_1"
        android:theme="@style/InflateActivityIncludeTheme" />
    <include
        layout="@layout/include_test_viewgroup"
        android:id="@+id/include_2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:theme="@style/InflateActivityIncludeTheme" />
</RelativeLayout>

两个子View的布局文件如下:

#include_test_merge.xml
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_centerInParent="true"
        android:background="?attr/colorPrimary"
        android:gravity="center"
        android:text="include merge"
        android:textColor="#fff"
        android:textSize="12sp" />
</merge>
#include_test_viewgroup.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_playcontroller"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="?attr/colorPrimary"
    android:gravity="center">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="include LinearLayout"
        android:textColor="#fff"
        android:textSize="15sp" />
</LinearLayout>

显现作用图如下:

rinflate源码分析

大致掩盖了上面说的几种include的办法

  1. theme中设置layout,<include />设置了theme能够不设置layout特点
  2. include子view是<merge />标签和非<merge />标签的区别
  3. <include />标签设置了width/height和其他方位相关的特点会运用外面设置的特点掩盖子View的特点,include_1没有设置特点所以运用的是include_test_viewgroup的特点,include_2设置了方位相关特点所以运用了设置的特点,从实际显现作用能看得出来。
  4. 关于theme的掩盖,假如子View设置了theme,会运用子View设置的theme替换context(父布局RelativeLayout)的theme,根据android:background="?attr/colorPrimary"能够看出来
总结:
  1. rinflate是递归办法,首要的递归判别条件是XmlPullParser的depth
  2. rinflate中判别了多种类型,有requestFocus和tag这些特别标签的处理,View的创立还是会调用createViewFromTag来处理
  3. 假如是include标签会运用parseInclude办法,由于<include />标签的特别性,会有一些<include />和真实标签的特点和theme的判别和替换
  4. <include />设置theme就替换掉父布局的theme,两种办法设置layout特点,标签中直接设置layout或者运用theme中的layout。
  5. <include />标签中设置了方位特点会替换子View的特点,<include />设置了id和visibility一定会收效。

参考: Android 中LayoutInflater(布局加载器)系列博文阐明