太长了,不想看?
LayoutInflater.inflate()
方法能将Xml格式的布局文件转换成以父子关系组合的一系列View,转换后的结构也称为View Hierarchy。
我们通常向inflate()方法传入三个参数:布局资源的resId,可为空的ViewGroup:root,以及布尔值attachToRoot。许多初学者对于后两个参数的使用比较生硬,缺乏理解。
我理解的引入root的原因是为了读取Xml文件中最外层标签的布局属性。布局属性就是我们常看到的以
开头的属性,他表示一个View希望在父布局中获得的尺寸以及位置,因此布局属性是提供给父布局读取以便计算尺寸、位置的。布局属性体现在View中就是mLayoutParams
变量,这个变量的类型是ViewGroup.LayoutParams
。不同的ViewGroup的子类实现了不同的LayoutParams
类,用以从Xml中读取自己关心的布局属性,一个View的mLayoutParams
的类型必须与其父布局实现的LayoutParams相一致。
对于Xml文件的最外层标签,他所转换成的View并不知道自己的父布局会是什么类型的,因此他不会生成mLayoutParams
变量,此时该标签的所有
属性都没有应用到View中。为了避免这种情况,我们需要告知最外层的View他的父布局是什么类型的,生成对应的LayoutParams储存布局属性。root就能帮助我们生成LayoutParams,而如果root正是我们希望的父布局,那么我们就将attachToRoot设为true,这样我们通过Xml生成的View Hierarchy可以直接加入到root中,我们不需要手动做这步操作了。
如果我们将Xml文件的嵌套结构看作是树状结构的话,逐个标签的解析其实就是树的深度优先遍历,我们在遍历的同时生成了一棵以View为节点,使用父子关系关联的树。
View Hierarchy中每个View的生成有四步:
- 由标签生成一个View
- 根据View的父布局的类型生成对应的LayoutParams,并将LayoutParams设置给View
- 生成View的所有Children
- 将View加入他的父布局中
由一个标签转换成一个View的过程其实就是通过ClassLoader加载出标签对应的View.Class文件并获得构造器,相当于调用View(Context context, @Nullable AttributeSet attrs)
构造一个实例。因此我们的自定义控件需要实现该构造方法才能在Xml中使用,随后从attrs变量中获得Xml中的属性。
inflate方法介绍
Android开发中,我们使用LayoutInflater.inflate()
方法将layout目录下Xml格式的资源文件转换为一个View Hierarchy,并返回一个View对象。
View Hierarchy直译大概就是视图层次,指的是以父子关系关联的一系列View,我们通过View Hierarchy的根节点(root view)可以获得该结构中所有的View对象。
如果让我们自己去设计一个方法将布局文件转换为View Hierarchy,我们可能会想到逐行读取Xml中的标签,利用标签的信息生成一个新的View/ViewGroup,当Xml中出现嵌套关系就意味我们需要使用父子关系关联两个View。而inflate()
方法做的就是这么一件事。
我们可以使用Activity.getLayoutInflater()
等方法获得一个LayoutInflater的实例,这些方法本质上都是通过getSystemService(Context.LAYOUT_INFLATER_SERVICE)
获得一个系统服务,这个方法最终会返回一个PhoneLayoutInflater
的实例。
inflate有几种重载方法,但最终都会走到
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
第一个参数我们看着比较陌生,但是大部分情况下我们只需要传入一个layout资源文件,Framework会帮我们根据资源获得一个Parser。需要注意的是为了提升使用时的效率,Android会在编译时就去解析layout资源文件并生成Parser,因此我们想通过inflate()方法在使用过程中使用一个单纯的Xml文件(非布局资源)去生成View是不可行的。
后面两个参数会在源码解析过程中介绍。
我们看下官方对这几个参数的定义:
参数 | 意义 |
---|---|
parser | XmlPullParser:以Pull的方式解析Xml文件,通过Parser对象方便我们操作 |
root | ViewGroup:可选项。当attachToRoot为true时,他将作为生成出来的View层级的parent。如果attachToRoot为false,那root仅仅为View层级树的根节点提供LayoutParams的值 |
attachToRoot | 配合root使用 |
inflate()方法的返回值是一个View,如果root不会为空且attachToRoot为true,返回root。否则返回Xml生成的View Hierarchy的根View。
Inflate 方法源码解析
看下inflate阶段的核心代码
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
// 解析根节点
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
// 将Xml最外层的标签解析为View对象,记为temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
if (root != null) {
// 当root不为空时,创建与root相匹配的LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 仅将params提供给temp
temp.setLayoutParams(params);
}
}
// 通过inflate生成temp的所有chiildre
rInflateChildren(parser, temp, attrs, true);
// root不为空且attachToRoot为true,将temp加入到root中,且用到params
if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root != null && attachToRoot)
return root;
}else {
return temp;
}
}
}
先不考虑
标签,inflate()
方法执行的流程:
- 将最外层的标签转换为一个View,记为temp。
- 当root不为空时,利用root生成的temp的LayoutParams。
- 解析Xml并生成temp的所有子View。
- 当root不为空且attachToRoot为true时,将temp添加为root的一个child。
- 当root不为空且attachToRoot为true时,返回root,否则返回temp。
这个流程包含了几个细节:
- 由Xml标签生成View对象
- 根据Xml嵌套结构生成View父子结构
- 应用root及attachToRoot
下面,我们由表及里的解析这几个细节。首先来看一下传参中root与attachToRoot两个参数的作用。
root与attachToRoot
root在inflate()方法中的第一个作用就是生成一个LayoutParams。
if (root != null) {
// 当root不为空时,创建与root相匹配的LayoutParams
params = root.generateLayoutParams(attrs);
}
LayoutParams保存的是一个控件的布局属性。那我们来看下为什么需要利用root生成LayoutParams。
布局属性与LayoutParams
在Xml文件中,有许多前缀为layout_
的属性,比如我们最熟悉的layout_width/layout_height
,我们称其为布局属性。View使用布局属性来告知父布局它所希望的尺寸与位置。不同类型的父布局读取的布局属性不同,比如layout_centerInParent
属性,父布局为RelativeLayout时会起作用,而父布局为LinearLayout时则无法使用。
Xml中的布局属性保存到View中就是mLayoutParams
变量,它的类型是ViewGroup.LayoutParams
。实际上ViewGroup的子类都会实现一个扩展自ViewGroup.LayoutParams
的嵌套类,这个LayoutParams类决定了他会读取哪些布局属性。我们看下RelativeLayout.LayoutParams
源码的一部分:
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.RelativeLayout_Layout);
...
final int N = a.getIndexCount();
for (int i = 0; i < N; i++) {
int attr = a.getIndex(i);
switch (attr) {
...
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
rules[LEFT_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
rules[RIGHT_OF] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
rules[ABOVE] = a.getResourceId(attr, 0);
break;
case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
break;
...
}
}
...
a.recycle();
}
这段代码跟我们自定义控件时读取Xml中的自定义属性是一样的做法,我们看到RelativeLayout.LayoutParams
在创建时读取了一系列布局属性并存储,比如layout_centerInParent
,其他的LayoutParams不会读取该属性。
如果对AttributeSet、TypedArray不熟悉可以参考这里:https://blog.csdn.net/lmj623565791/article/details/45022631
我们能在RelativeLayout.onMeasure()
方法中找到对LayoutParams的使用。
// RelativeLayout.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
int count = views.length;
for (int i = 0; i < count; i++) {
View child = views[i];
if (child.getVisibility() != GONE) {
...
LayoutParams params = (LayoutParams) child.getLayoutParams();
int[] rules = params.getRules(layoutDirection);
...
}
}
...
}
当计算每个子View的位置时,需要读取他们的布局属性,此时会将View的LayoutParams强制类型转换为RelativeLayout.LayoutParams,这里可能会报出无法转换类型的错误,所以我们需要保证加入到RelativeLayout的View的LayoutParams类型都是RelativeLayout.LayoutParams
。
我们总结一下LayoutParams的核心知识点:
- LayoutParams保存了Xml中的
layout_
开头的布局属性 - ViewGroup子类通常会实现一个LayoutParams类,用于读取他们需要的布局属性
- View的LayoutParams类型必须与其父布局的类型相匹配,否则会在
onMeasure
过程中报错
回到root与attachToRoot
当我们解析Xml中最外层的标签,也就是View Hierarchy的根View时,程序并不知道它的父布局会是什么类型的,因此不会生成LayoutParams。这时最外层标签中的所有布局属性,包括layout_width/layout_height
都不会被记录到View对象中,也就是俗称的“属性失效了”。
但在实际使用中,我们通常能知道Xml生成的View Hierarchy所要加入的父布局或是要加入的父布局的类型。这时候我们传入一个root
参数,根据root的类型去读取根View的布局属性并生成对应的LayoutParams
。这段代码如下:
if (root != null) {
// root不为空时,生成与root对应的LayoutParams
params = root.generateLayoutParams(attrs);
}
public LayoutParams generateLayoutParams(AttributeSet attrs) {
// 不同的ViewGroup生成LayoutParams的细节不同
return new LayoutParams(getContext(), attrs);
}
而attachToRoot
则用于判断root是直接作为parent使用还是仅需要他的类型信息。
if (root != null) {
if (attachToRoot) {
root.addView(temp, params);
}else{
// Set the layout params for temp if we are not attaching.
temp.setLayoutParams(params);
}
}
递归处理Xml中的所有标签
在inflate
方法中,除了根标签以外所有剩余标签的解析只使用了一个方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
...
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
...
}
final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
boolean finishInflate) throws XmlPullParserException, IOException {
rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}
这个方法仅仅是调用rInflate
方法,没有其他额外的行为。rInflate方法的中r
的含义是Recursive
,即递归,我们可以猜测这个方法是使用递归的方式去处理Xml中的所有嵌套关系。我们看下rInflate
核心部分:
这段方法涉及到XmlPullParser的知识,他将Xml文件转换为一个对象。通过
next()
方法获得下一个事件,一共有五个事件START_DOCUMENT、START_TAG、TEXT、END_TAG、END_DOCUMENT。并且通过depth取得当前元素嵌套的深度,未读取到START_TAG时depth为0,每次读取到START_TAG时depth加1。细节参考 http://www.xmlpull.org/ 。
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) {
final int depth = parser.getDepth();
// while的结束条件:找到该depth的END_TAG(或者文件结束)
while (((type = parser.next()) != XmlPullParser.END_TAG
|| parser.getDepth() > depth)
&& type != XmlPullParser.END_DOCUMENT) {
// 只有遇到START_TAG时进行解析
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
...
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException(" cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException(" must be the root element");
} else {
// 将当前Tag转换为View
final View view = createViewFromTag(parent, name, context, attrs);
// 根据父布局类型读取布局属性
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 嵌套处理,解析当前View的所有children
rInflateChildren(parser, view, attrs, true);
// 将View加入到parent中
viewGroup.addView(view, params);
}
}
}
我们配合例子分析下rInflate
方法的流程:
1.首先记下当前的depth
2.取Xml中的下一个事件,直到到达文件的结尾,或者到达了当前depth的END_TAG
参照例子,如果rInflate开始时解析到了
3. while循环中,遇到START_TAG时解析
只解析STAST_TAG是保证每个Tag都只生成一个View,比如在解析到
解析普通的TAG的逻辑如下:
// 将当前Tag转换为View
final View view = createViewFromTag(parent, name, context, attrs);
// 根据父布局类型读取布局属性
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
// 嵌套处理,解析当前View的所有children
rInflateChildren(parser, view, attrs, true);
// 将View加入到parent中
viewGroup.addView(view, params);
这里需要关注的就是那个嵌套的rInflateChildren()
方法,他也会走到rInflate,以当前解析出的View作为Parent,解析下一层的内容。
inflate过程推演
我们还是以上面的例子将inflate方法的执行流程推演一遍。
- inflate方法,取到
,解析出LayoutA对象 - 通过rInflate方法解析LayoutA的所有children,开始时depth为1
- 取到
,解析出ViewB对象,ViewB无children,它的rInflateChildren会读到ViewB的END_TAG并结束,将ViewB加入到LayoutA中 - 取到
,解析出LayoutC对象,调用rInflate方法解析Children - 取到
,解析出ViewC对象,无Children,将ViewC加入到LayoutC中 - 取到
,LayoutC的rInflateChildren过程结束 - 将LayoutC对象加入到LayoutA中
- 取到
- 取到,为END_TAG且depth为1,rInflate方法结束
- 取到
- View Hierarchy解析完成,配合root及attachToRoot做些处理便可返回
整个流程自上而下的解析了Xml文件中的所有Tag,并生成了对应的View Hierarchy,嵌套关系顺利转换成了父子关系。生成的View Hierarchy是一个树状结构,生成过程跟树的深度优先遍历有相似的感觉。
merge标签解析
解析完rInflate方法后,我们再来看下
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
}
其实就是忽略
由Tag生成对应的View
前面我们介绍了layout Xml如何转换为View Hierarchy,这个过程中的最后一个细节就是单个Tag如何转化成View对象
无论是inflate方法对根View的解析还是rInflate中的嵌套解析,都是调用createViewFromTag()
方法,我们看下这个方法的核心部分。
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
// 标签中可以使用class来标注类
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// 除了以外,ignoreThemeAttr总为ture,读取Xml中的theme并通过Context作用到View上
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
// 就当是彩蛋吧,let's party
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
// 生成View
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
// 用于构建View的参数
// View(Context context, @Nullable AttributeSet attrs)
// 第一个传参是context
attrs, int defStyleAttr)
mConstructorArgs[0] = context;
try {
// 使用系统控件时我们可以不带着命名空间,此时name中不包含"."
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
直接看到生成View的部分
- 由Factory2生成
- 由Factory生成
- 由mPrivateFactory生成
- 由onCreateView/createView方法生成
这里的生成方法有先后顺序,View一旦生成就不用走后面的方法。Factory2及Factory可以看做是给我们hook代码的,允许我们按自己的期望去将Tag转换为View,二者的区别是工厂方法的传参不同。
如果没有设置工厂
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
Xml中使用系统控件可以不加上命名空间,因此name中没有“.”,在onCreateView方法中会为系统控件加上前缀“"android.view."”并调用createView方法。而我们前面提到了我们实际使用的LayoutInflater通常是PhoneLayoutInflater
,他重写了onCreateView方法:
/// PhoneLayoutInflater.java
@Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
for (String prefix : sClassPrefixList) {
try {
View view = createView(name, prefix, attrs);
if (view != null) {
return view;
}
} catch (ClassNotFoundException e) {
// In this case we want to let the base class take a crack
// at it.
}
}
return super.onCreateView(name, attrs);
}
private static final String[] sClassPrefixList = {
"android.widget.",
"android.webkit.",
"android.app."
};
/// LayoutInflater.java
protected View onCreateView(String name, AttributeSet attrs)
throws ClassNotFoundException {
return createView(name, "android.view.", attrs);
}
这里的操作都是为系统控件补全命名空间,具体的生成View的工作由createView完成,核心代码如下:
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
...
// 使用ClassLoader获得构造器
clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
// View的构造器的传参
Object[] args = mConstructorArgs;
args[1] = attrs;
// 获得View实例
final View view = constructor.newInstance(args);
return view;
...
}
先通过ClassLoader加载类,获得构造器,然后实例化View的子类。具体的构造方法是View(Context context, @Nullable AttributeSet attrs)
,因此我们的自定义控件也需要实现这个构造方法才能在Xml中正确使用。
`