您的位置:首页 > 移动开发 > Android开发

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

2017-10-08 17:29 656 查看
本文出自博客Vander丶CSDN博客,如需转载请标明出处,尊重原创谢谢

博客地址:http://blog.csdn.net/l540675759/article/details/78176074

前言

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

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

导航

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

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

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

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

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

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

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

概述

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

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

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

获取include标签中的layout资源

处理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属性,接下来看一下显示效果:



从效果图可以发现,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的分析就到此结束。

流程图

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐