对于 LayoutInflater 这个类,想必大家并不陌生。因为当我们学习 RecyclerList 这个好用的列表时,需要为这个列表编写适配器,在适配器里有这么两个覆写的方法,让我们在初学时很摸不着头脑:
1 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |
我在初学时看到这两行代码也是眼冒金星,尤其是对于下面这行代码
1 | LayoutInflater.from(parent.context) |
这让我产生了三个问题:
- LayoutInflater.from(parent.context)返回的是什么,为什么它的参数要传入 parent.context
- inflate(R.layout.fruit_item, parent, false)的参数是返回了一个 View,然后这个 onCreateViewHolder 函数又以它构造了 ViewHolder 返回了,最后这个 ViewHolder 拿去干什么了?
- 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)来获取。
- 为什么要传入 context:每个 context 只需要对应一个单例 LayoutInflater,因而传入 context 是用来表明需要获取哪个 context 的 LayoutInflater 对象
- 为什么要使用静态工厂方法:实现良好的接口封装,用户不需要知道实现了 LayoutInflater 这个抽象类的子类是什么,且这个子类对用户不可见,用户只需要关注 LayoutInflater 的功能即可。具体可看另一篇文章:Java 创建对象的深入做法
这样,我们就回答了第一个问题。
inflate 的使用和执行过程
首先我们先来看 inflate 这个方法的定义
1 | public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) |
- resource:类型为 int,代表要加载的布局在 R 文件中的值,通常为 R.layout.xxx
- root:类型为 ViewGroup,若由 resource 加载出来的 View 为 temp,那么 root 便是 temp 的父 View
- attachToRoot:类型为 boolean,代表是否要直接将该 View 添加到 root 的 children 列表里
第一个参数很好理解,比较难以理解的是后两个参数。在解释这两个参数的用法前,我先简要地介绍两个重要概念:
- 每一个 View 有自己的 MeasureSpec,attr,它们都是通过读取 xml 文件生成的,并且都不是具体的布局参数(布局参数是用来指导子 View 相对父 View 是怎样放置的,即该 View 相对父 View 的位置)
- LayoutParams 是 View 中的布局参数,它的计算是根据父 View 的 MeasureSpec 和自己的 attr 来计算的
总而言之,一个 View 只要有父 View,就会有 LayoutParams 参数
那么接下来,我们结合源码来分析一下。
1 | public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { |
其中得到完整的由目标布局加载的 View 的两行代码是:
1 | //创建最外层 View |
最终 temp 即是我们根据目标布局新创建的 View
从源码中我们看出,inflate 这个函数后两个参数的 4 种情况是怎么处理的:
- root=null,attachRoot=false:这种状况很好理解,我们要创建的 View 并没有指定的父 View,只是返回由 resource 创建的 View 便可以。因此它的返回值就是 temp,temp 的 LayoutParams 参数为 null
- root=null,attachRoot=true:这种状况是被禁止的,因为它没有意义,因为你没有指定一个父 View,还想把它直接加到父 View 的 children 列表里
- root!=null。attachRoot=true:这种状况也很好理解,我们为它指定了父 View,也希望将它直接加入父 View 的 children 列表里。在这种情况下,它会根据父 View(root)计算好 temp 的 LayoutParams 的值,然后直接将 temp 加入父 View 的 children 列表里,最后将 root 返回
- root!=null,attachRoot=false。这种情况稍微难以理解,它指定了父 View,但不希望将它直接加入父 View 的 children 列表里。在这种情况下,它仅仅会根据父 View(root)计算好 temp 的 LayoutParams 的值,然后直接将 temp 返回
至于在 createViewFromTag(root, name, inflaterContext, attrs)这个函数里,究竟是怎样通过反射读取文件创建并返回 View 的,我们在这里不深入讨论,有兴趣可以去看一下这篇文章:Android | 带你探究 LayoutInflater 布局解析原理,从源码的角度讲得非常详细。
至此,我们回答了第三个问题,就剩下第二个问题了。
RecyclerList 适配器的覆写函数
让我们重新看这两个覆写函数
1 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |
首先看第一个函数,经过上面的介绍,我们可以很清晰地得出,这个函数先使用 LayoutInflater 加载了 RecyclerList 每个子项的布局,并指定其父 View 为参数 parent,获得其 View,再构造 ViewHolder 返回。
由此我们可以做出下面两个猜想
- parent 的参数就是 RecyclerList 这个 View 本身,因为 RecyclerList 的每个子项的父 View 应该就是它本身。这点我们在源码里找到了答案:
1 | holder = mAdapter.createViewHolder(RecyclerView.this, type); |
- 这个 ViewHolder 按需创建(屏幕当前需要展示多少个 item 就创建多少个),然后将其放到了某个 List 里,RecyclerList 再在合适的时机将它们加入到其子 View(children)列表中,便完成了一个列表 View 的创建。这点我们也在源码中找到了相应的佐证:
1 | final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); |
至于第二个函数,很明显,它是在 onCreateViewHolder 函数之后执行,根据 position 的对应关系,将 list 的内容填入每个创建的 item 的 View 中。
至此,第三个问题全部回答完毕,相信你之后再写 RecyclerList 的适配器时,再也不用去回忆该怎么写了。