Android 中LayoutInflater(布局加载器)源码篇之parseInclude方法

528 阅读10分钟

本文出自博客Vander丶CSDN博客,如需转载请标明出处,尊重原创谢谢
博客地址:blog.csdn.net/l540675759/…

前言

如果读者没有阅读过该系列博客,建议先阅读下博文说明,这样会对后续的阅读博客思路上会有一个清晰的认识。

Android中LayoutInflater(布局加载器)系列博文说明


导航

Android 中LayoutInflater(布局加载器)系列博文说明

Android 中LayoutInflater(布局加载器)系列之介绍篇

Android 中LayoutInflater(布局加载器)系列之源码篇

Android 中LayoutInflater(布局加载器)源码篇之createViewFromTag方法

Android 中LayoutInflater(布局加载器)源码篇之rInflate方法

Android 中LayoutInflater(布局加载器)源码篇之parseInclude方法

Android 中LayoutInflater(布局加载器)之实战篇


概述

本篇博客,是作为Android中LayoutInflater(布局加载器)源码篇的一个补充,至此LayoutInflater中几个大模块在这个系列的博文中,已经分析完毕了。

本篇专门介绍解析《include》标签的解析流程,具体分成以下几部分:

  1. include标签涉及到theme时的相关处理

  2. 获取include标签中的layout资源

  3. 处理include包裹的内容


parseInclude()是在哪里使用的?

 void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
            //----------------省略部分代码--------------------//

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

从上来代码中,可以发现parseInclude()是在rInflate()中出现,作用是处理当前节点是Include标签时的状况。

而rInflater()这个方法的作用是,解析某个节点,根据节点的不同类型从而进行不同的处理,如果想深入了解可以参考这篇博客:

Android 中LayoutInflater(布局加载器)源码篇之rInflate方法


parseInclude()源码解析

 //参数说明:
 // parser      解析布局的解析器
 // context     当前加载布局的上下文对象
 // parent      父容器
 // attrs       属性集合(XML该节点的属性集合)
 private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;

        // 判断 Include标签是否在 ViewGroup容器之内,因为 include 标签只能存在于 ViewGroup 容器之内。

        if (parent instanceof ViewGroup) {

            //------------------<第一部分>-------------------//

            //当开发者设置 include 主题属性时,可以覆盖被 include 包裹View的主题属性。
            //但是这种操作很少会使用。
            //所以如果被包裹 View 设置主题属性,我们在设置就会出现覆盖效果。
            //以 include 标签的主题属性为最终的主题属性

            //提取出 include 的 thme 属性,如果设置了 them 属性,那么include 包裹的View 设置的 theme 将会无效
            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();


            //------------------<第二部分>-------------------//

            //如果这个属性是指向主题中的某个属性,我们必须设法得到主题中layout 的资源标识符
            //先获取 layout 属性(资源 id)是否设置
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            if (layout == 0) {
            //如果没直接设置布局的资源 id,那么就检索?attr/name这一类的 layout 属性
                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/name 这一类的属性中,获取布局属性  
                layout = context.getResources().getIdentifier(value.substring(1), null, null);
            }

            //这个布局资源也许存在主题属性中,所以需要去主题属性中解析
            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.");
            } else {
                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(childParser.getPositionDescription() +
                                ": No start tag found!");
                    }

                    final String childName = childParser.getName();

                    if (TAG_MERGE.equals(childName)) {
                        //解析 Meger 标签
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else {
                        //根据 name名称来创建View
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        final ViewGroup group = (ViewGroup) parent;


                        //获取 View 的 id 和其 Visiable 属性
                        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();

                        //需要将 Parent中的 LayoutParams 设置为其 Params 属性。
                        //如果 Parent 没有通用的 Params,那么就会抛出Runtime 异常

                        //然后会为其设置 include 包裹内容的通用 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);

                        // 解析子标签
                        rInflateChildren(childParser, view, childAttrs, true);

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

                        // 加载include内容时,需要直接设置其 可见性
                        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();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }

        LayoutInflater.consumeChildElements(parser);
    }

先把parseInclude()这个方法全景先看下,然后我们在进行分拆,一部分一部分分析。


parseInclude()参数解读

parseInclude()中分别含义四个参数:

(1)解析器 -> XmlPullParser parser

用来解析XML文件的解析器,通过解析器可以得到当前节点的相对应的AttributeSet(属性集)

(2)上下文对象 - > Context context

当前加载该XML的上下文对象,并且这个Context与LayoutInflater属于相互绑定关系(一一对应)

(3)父容器 - > View parent

包裹该节点的父容器,一般来说都是继承ViewGroup实现的视图组

(4)属性集 -> AttributeSet attrs

该节点的属性集,包括所有该节点的相关属性


Include中的theme属性

这里大家先了解一个相关的问题,关于include标签设置theme属性的情况:

一般来说theme(主题)一般出现在Activtiy的AndroidManifest文件下,来给Activity设置统一的布局效果,而且可以使用如下的操作来进行主题属性的使用。

//  ?attr这样的形式,使用主题中的设置参数
android:background="?attr/colorPrimary"

如果Include标签下设置了新的theme,那么Include中的内容在使用主题属性时,使用的theme主题就是(include)设置的内容,而不是Activity默认下的主题,形成了一种覆盖效果。

也就是说Include标签设置的主题可以覆盖Activity设置的根主题,但是Include设置的主题只作用与Include内部。


举个栗子:

style.xml

先定义好两个基础Theme,一个是作为App的基础主题,另一个是include中的主题。

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- BaseApplication theme -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>


    <style name="IncludeTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Include Theme -->
        <item name="colorPrimary">@color/colorAccent</item>
        <item name="colorPrimaryDark">@color/colorAccent</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

AndroidManifest.xml

设置Activity的基础主题为AppTheme

        <activity
            android:name="com.demo.MainActivity"
            android:theme="@style/AppTheme"></activity>

activity_main.xml

<?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:orientation="vertical">

    <!-- 这里是使用基础Theme的Toolbar -->
    <android.support.v7.widget.Toolbar
        android:id="@+id/activity_theme_tb"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="?attr/colorPrimary" />

    <!-- 这里是自带Theme Include的Toolbar -->
    <include
        layout="@layout/test_toolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:theme="@style/IncludeTheme" />

</RelativeLayout>

接下来,我们在看一下Include包裹的布局
test_toolbar.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <android.support.v7.widget.Toolbar
        android:id="@+id/include_toolbar"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="?attr/colorPrimary" />

</LinearLayout>

从上面的XML文件我们可以看出两个Toolbar调用的background都指向theme的colorPrimary属性,接下来看一下显示效果:

Toolbar对比图

从效果图可以发现,Include Toolbar显示的颜色是粉色的,也就是Include额外设置的theme,这里也是从正面证明了这个概念。


第一部分:Include Theme主题的设置

            //------------------<第一部分>-------------------//
            //提取出Theme属性
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            final boolean hasThemeOverride = themeResId != 0;
            //如果存在Theme属性,那么Include包含的子标签都会使用该主题
            if (hasThemeOverride) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();

通过上面的介绍,很明显这段代码含义,就是检测是否给Include标签设置了Theme属性,如果设置theme,就创建相应的ContextThemeWrapper,用于之后子标签的解析时theme的使用。


第二部分:Include 内容布局的设置

            //------------------<第二部分>-------------------//
            //先获取 layout 属性(资源 id)是否设置
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            if (layout == 0) {
            //如果没直接设置布局的资源 id,那么就检索?attr/name这一类的 layout 属性
                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/name 这一类的属性中,获取布局属性  
                layout = context.getResources().getIdentifier(value.substring(1), null, null);
            }

            //这个布局资源也许存在主题属性中,所以需要去主题属性中解析
            if (mTempValue == null) {
                mTempValue = new TypedValue();
            }
            if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
                layout = mTempValue.resourceId;
            }

这部分的内容主要是提取Include的内容布局的提取,Include的内容布局的设置有两种:

第一种 : 直接@layout 后面设置布局的XML

        layout="@layout/test_toolbar"

第二种:通过引入theme的item设置的layout属性

Include标签下:

        layout="?attr/theme_layout"

包裹Include标签的布局Theme(注意:这里不是Include设置的主题):

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        //重点在这里!!!!!
        <item name="theme_layout">@layout/test_toolbar</item>
    </style>

而上面的代码的作用是检索layout属性,如果layout已经以第一种方式引入,就不需要在去theme中检索,如果layout第一种方式检索不到资源ID,那么就会去以第二种方式进行检索。


第三部分: Include标签的View处理

            //------------------<第三部分>-------------------//
            //如果此时还找不到layout,那么必然异常~,会报找不到资源ID的layout异常
            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.");
            } else {
            //生成子解析器
                final XmlResourceParser childParser = context.getResources().getLayout(layout);

                try {
                    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
                    //----------------省略了XML一些规则的判断----------------//
                    //获取子节点的名称
                    final String childName = childParser.getName();
                    if (TAG_MERGE.equals(childName)) {
                        //解析 Meger 标签
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else {
                        //根据 name名称来创建View
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        final ViewGroup group = (ViewGroup) parent;
                        //获取 View 的 id 和其 Visiable 属性
                        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();

                        //需要将 Parent中的 LayoutParams 设置为其 Params 属性。
                        //如果 Parent 没有通用的 Params,那么就会抛出Runtime 异常

                        //然后会为其设置 include 包裹内容的通用 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);

                        // 解析子标签
                        rInflateChildren(childParser, view, childAttrs, true);

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

                        // 加载include内容时,需要直接设置其 可见性
                        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();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }

这部分主要的作用是解析Include包裹layout的根标签:

(1)先特别处理Merge标签 :

如果子节点是Merge标签,那么直接进行内容的解析,调用rInflater()方法。

而rInflater()这个方法的作用是,解析某个节点,根据节点的不同类型从而进行不同的处理,如果想深入了解可以参考这篇博客:

Android 中LayoutInflater(布局加载器)源码篇之rInflate方法

(2)解析Include的内容:

在这之前先通过createViewFromTag()方法,根据名称来生成相对应的View,具体的解析请参考这篇博客:

Android 中LayoutInflater(布局加载器)源码篇之createViewFromTag方法

这里分成两块内容,第一块是设置LayoutParams:

                        ViewGroup.LayoutParams params = null;
                        try {
                            //加载Include的父ViewGroup的LayoutParams
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        if (params == null) {
                            //加载Include的子ViewGroup的LayoutParams
                            params = group.generateLayoutParams(childAttrs);
                        }
                        view.setLayoutParams(params);

这段的作用是为Include的包裹的根View设置LayoutParams,使用的LayoutParams默认是Include外层的ViewGroup。

如果此时Params加载失败,那就会使用Include包裹的ViewGroup的LayoutParams,反正怎么都得设置一个。

第二块是在这里设置子ViewGroup的显隐性:

                        // 加载include内容时,需要直接设置其 可见性
                        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);
                    }

设置ViewGroup的显隐性,之后就将其添加至父View中,至此parseInclude的分析就到此结束。


流程图

parseInclude的流程图