安卓布局加载之LayoutInflater

冰岩作坊 November 22, 2024

对于 LayoutInflater 这个类,想必大家并不陌生。因为当我们学习 RecyclerList 这个好用的列表时,需要为这个列表编写适配器,在适配器里有这么两个覆写的方法,让我们在初学时很摸不着头脑:

1
2
3
4
5
6
7
8
9
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitName.text = fruit.name
}

我在初学时看到这两行代码也是眼冒金星,尤其是对于下面这行代码

1
2
3
LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item, parent, false)

这让我产生了三个问题:

要解答这三个问题,我们就要去学习 LayoutInflater 这个类以及它背后的布局加载功能。

首先,我们要知道 LayoutInflater 是什么。简而言之,它就是将我们编写的布局 xml 文件文件加载成 View 的加载器。由于我们编写的移动软件都是在 Java 上运行的,显然 xml 这样的文件并不能运行。安卓的视图都是基于 View 类去显示的,屏幕的每次的绘制流程都是从 Activity 产生的 DecorView 开始递归遍历 View 树来进行绘制。但 View 类是十分复杂的,它的参数十分繁多,让我们直接在代码里去 new 一个 View 对象是非常不现实且不直观的。因而安卓的设计者想到了一个办法:我们可以提前编写一些静态文件,里面按照约定的格式编写好一个 View 的参数,再利用 Java 的反射特性,在运行时去读取该文件,生成相应的 View 对象返回。在这个过程中,这个静态文件就是 xml 文件,而读取该文件,生成相应的 View 对象返回的工作,就交给了 LayoutInflater 这个类。

LayoutInflater 类怎么获取

LayoutInflater 类是一个抽象类,因此我们不能直接构造它的对象,只能通过其提供的静态工厂方法 LayoutInflater.from(context)来获取。

这样,我们就回答了第一个问题。

inflate 的使用和执行过程

首先我们先来看 inflate 这个方法的定义

1
2
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

第一个参数很好理解,比较难以理解的是后两个参数。在解释这两个参数的用法前,我先简要地介绍两个重要概念:

总而言之,一个 View 只要有父 View,就会有 LayoutParams 参数

那么接下来,我们结合源码来分析一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
1. 结果变量
View result = root;
2. 最外层的标签
final String name = parser.getName();
3. <merge>
if (TAG_MERGE.equals(name)) {
3.1 异常
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
3.2 递归执行解析
rInflate(parser, root, inflaterContext, attrs, false);
} else {
4.1 创建最外层 View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

ViewGroup.LayoutParams params = null;

if (root != null) {
4.2 创建匹配的 LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
4.3 如果 attachToRoot 为 false,设置LayoutParams
temp.setLayoutParams(params);
}
}

5.temp 为 root,递归执行解析
rInflateChildren(parser, temp, attrs, true);

6. attachToRoot 为 true,addView()
if (root != null && attachToRoot) {
root.addView(temp, params);
}

7. root 为空 或者 attachToRoot 为 false,返回 temp if (root == null || !attachToRoot) {
result = temp;
}
}
return result;
}

其中得到完整的由目标布局加载的 View 的两行代码是:

1
2
3
4
5
6
//创建最外层 View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//以 temp 为 root,递归执行解析
rInflateChildren(parser, temp, attrs, true);


最终 temp 即是我们根据目标布局新创建的 View

从源码中我们看出,inflate 这个函数后两个参数的 4 种情况是怎么处理的:

至于在 createViewFromTag(root, name, inflaterContext, attrs)这个函数里,究竟是怎样通过反射读取文件创建并返回 View 的,我们在这里不深入讨论,有兴趣可以去看一下这篇文章:Android | 带你探究 LayoutInflater 布局解析原理,从源码的角度讲得非常详细。

至此,我们回答了第三个问题,就剩下第二个问题了。

RecyclerList 适配器的覆写函数

让我们重新看这两个覆写函数

1
2
3
4
5
6
7
8
9
10
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitName.text = fruit.name
}

首先看第一个函数,经过上面的介绍,我们可以很清晰地得出,这个函数先使用 LayoutInflater 加载了 RecyclerList 每个子项的布局,并指定其父 View 为参数 parent,获得其 View,再构造 ViewHolder 返回。

由此我们可以做出下面两个猜想

1
2
holder = mAdapter.createViewHolder(RecyclerView.this, type);

1
2
3
4
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
...
ChangedScrap.add(holder);

至于第二个函数,很明显,它是在 onCreateViewHolder 函数之后执行,根据 position 的对应关系,将 list 的内容填入每个创建的 item 的 View 中。

至此,第三个问题全部回答完毕,相信你之后再写 RecyclerList 的适配器时,再也不用去回忆该怎么写了。

#Android