Android 布局打气筒 (一):玩转 LayoutInflater
前言
很高兴遇见你~
今天准备和大家分享的是 LayoutInflater,我给它取名:布局打气筒,很形象,其实就是根据英文翻译过来的😂。我们知道气球打气筒可以给气球打气从而改变它的形状。而布局打气筒的作用就是给我们的 Xml 布局打气让它变成一个个 View 对象。在我们的日常工作中,经常会接触到他,因为只要你写了 Xml 布局,你就要使用 LayoutInflater,下面我们就来好好讲讲它。
注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析
一、基本使用
1、LayoutInflater 实例获取
1)、通过 LayoutInflater 的静态方法 from 获取
2)、通过系统服务 getSystemService 方法获取
3)、如果是在 Activity 或 Fragment 可直接获取到实例
//1、通过 LayoutInflater 的静态方法 from 获取
val layoutInflater: LayoutInflater = LayoutInflater.from(this)
//2、通过系统服务 getSystemService 方法获取
val layoutInflater: LayoutInflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
//3、如果是在 Activity 或 Fragment 可直接获取到实例
layoutInflater //相当于调用 getLayoutInflater()
实际上,1 是 2 的简单写法,只是 Android 给我们做了一下封装。拿到 LayoutInflater 实例后,我们就可以调用它的 inflate 系列方法了,这几个方法是本篇文章的一个重点,如下:
从 Xml 布局到创建 View 对象,这几个方法扮演着至关重要的作用,其中我们用的最多就是第一个和第三个重载方法,现在我们就来使用一下
二、例子
1、创建一个新项目,MainActivity 对应的布局如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cons_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"/>
2、创建一个新的布局取名 item_main.xml,如下图:
3、修改 MainActivity 中的代码
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val consMain = findViewById<ConstraintLayout>(R.id.cons_main)
val itemMain = layoutInflater.inflate(R.layout.item_main, null)
consMain.addView(itemMain)
}
}
上述代码我们使用了两个参数的 inflate 重载方法,第二个参数 root 传了一个 null ,然后把当前布局添加到 Activity 中,运行看下效果:
啥情况?怎么和预想的不一样呢?我的背景颜色怎么不见了?把这个问题 1 先记着
接下来,我们修改一下 MainActivity 中的代码,如下:
val itemMain = layoutInflater.inflate(R.layout.item_main, consMain)
//等同下面这行代码
val itemMain = layoutInflater.inflate(R.layout.item_main, consMain,true)
实际上上面这句代码就相当于调用了三个参数的重载方法,且第三个参数为 true,我们看下它两个参数的源码:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
现在在运行看下结果:
报错了,提示我们当前 child 已经有了一个父 View,你必须先调用父 View 的 removeView 方法移除当前 child 才行。是不是疑问更多了呢?把这个问题 2 也先记着
我们在修改一下 MainActivity 中的代码,如下:
val itemMain = layoutInflater.inflate(R.layout.item_main, consMain,false)
在运行看下结果:
嗯,现在达到了我们预期的效果
现在回到上面那两个问题,分析发现是 LayoutInflater inflate 方法传了不同的参数导致的,那这些参数到底有什么玄乎的地方呢?接下来跟着我的脚步分析下源码,或许你就豁然开朗了
三、LayoutInflater inflate 系列方法源码分析
在分析源码之前,我们需要明白一些基础知识:
我们一般都会使用 layout_width 和 layout_height 来设置 View 的大小,实际上是要满足一个条件,那就是这个 View 必须存在于一个容器或布局中,否则没有意义,之后如果将 layout_width 设置成 match_parent 表示让 View 的宽度填充满布局,如果设置成 wrap_content 表示让 View 的宽度刚好可以包含其内容,如果设置成具体的数值则 View 的宽度会变成相应的数值。这也是为什么这两个属性叫作 layout_width 和layout_height,而不是 width 和 height 。
明白了上面这些知识,我们继续往下看
实际上,我们调用 LayoutInflater inflate 系列方法,最终都会走到上述截图的第 4 个重载方法,看下它的源码,仅贴出关键代码:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
//...
//获取布局 Xml 里面的属性集合
AttributeSet attrs = Xml.asAttributeSet(parser);
// 将传入的 root 赋值 给 result
View result = root;
// 创建根 View 赋值给 temp
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
//...
//如果传入的 root 不为空,通过 root 和布局属性生成布局参数
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果传入的 attachToRoot 为 false 则给当前创建的根 View 设置布局参数
temp.setLayoutParams(params);
}
}
//递归创建子 View 并添加到父布局中
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
//如果 root 不为空且 attachToRoot 为 true,添加当前创建的根 View 到 root
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
//如果 root 为空或者 attachToRoot 为 false, 将当前创建的根 View 赋值给 result
result = temp;
}
//...
//返回当前 result
return result;
}
}
上述代码我们可以得到一些结论:
1、如果传入的 root 不为 null 且 attachToRoot 为 false,此时会给 Xml 布局生成的根 View 设置布局参数
注意:Xml 布局生成的根 View 并没有被添加到任何其他 View 中,此时根 View 的布局属性不会生效,但是我们给它设置了布局参数,那么它就会生效,只是没有被添加到任何其他 View 中
2、如果传入的 root 不为 null 且 attachToRoot 为 true,此时会将 Xml 布局生成的根 View 通过 addView 方法携带布局参数添加到 root 中
注意:此时 Xml 布局生成的根 View 已经被添加到其他 View 中,注意避免重复添加而报错
3、如果传入的 root 为 null ,此时会将 Xml 布局生成的根 View 对象直接返回
注意:此时 Xml 布局生成的根 View 既没有被添加到其他 View 中,也没有设置布局参数,那么它的布局参数将会失效
明白了上面这些知识点,我们在看下为啥为会出现之前那些问题
四、问题分析
1、问题 1
上述问题 1 实际上我们是调用了 LayoutInflater 两个参数的 inflate 重载方法:
inflate(@LayoutRes int resource, @Nullable ViewGroup root)
传入的实参: resouce 传入了一个 Xml 布局,root 传入了 null
根据我们上面源码得到的结论,当传入的 root 为 null ,此时会将 Xml 布局生成的根 View 对象直接返回
那么此时这个布局根 View 不在任何 View 中,因此它的布局属性失效了,但是 TextView 在一个布局中,它的布局属性会生效,因此就出现了上述截图中的效果
2、问题 2
上述问题 2 我们调用的还是 LayoutInflater 两个参数的构造方法
传入的实参: resouce 传入了一个 Xml 布局,root 传入了 consMain
实际又会调用 LayoutInflater 三个参数的 inflate 重载方法:
inflate(@LayoutRes int resource, @Nullable ViewGroup root,boolean attachToRoot)
此时传入实参变为:resouce 传入了一个 Xml 布局,root 传入了 consMain,attachToRoot 传入了 true
根据我们上面源码得到的结论:当传入的 root 不为 null 且 attachToRoot 为 true,此时会将 Xml 布局生成的根 View 通过 addView 方法携带布局参数添加到 root 中
此时我们在 MainActivity 中又重复调用了 addView 方法,因此就报那个错了。如果想不报错,把 MainActivity 中的那行 addView 去掉就可以了
3、预期效果
上述预期效果,我们调用的是 LayoutInflater 三个参数的 inflate 重载方法
传入的实参:resouce 传入了一个 Xml 布局,root 传入了 consMain,attachToRoot 传入了 false
根据我们上面源码得到的结论:当传入的 root 不为 null 且 attachToRoot 为 false,此时会给 Xml 布局生成的根 View 对象设置布局参数
此时根 View 的布局属性会生效,只不过没有被添加到任何 View 中,而又因为 MainActivity 中调用了 addView 方法,把当前根 View 添加了进去,所以达到了我们预期的效果
到这里,你是否明白了 LayoutInflater inflate 方法的应用了呢?
如果还有疑问,欢迎评论区给我提问,我们一起讨论
五、为啥 Activity 中布局根 View 的布局属性会生效?
看下面这张图:
注意:Android 版本号和应用主题会影响到 Activity 页面组成,这里以常见页面为例
我们的页面中有一个顶级 View 叫 DecorView,DecorView 中包含一个竖直方向的 LinearLayout,LinearLayout 由两部分组成,第一部分是标题栏,第二部分是内容栏,内容栏是一个FrameLayout,我们在 Activity 中调用 setContentView 就是将 View 添加到这个FrameLayout 中。
看到这里你应该也明白了:Activity 中布局根 View 的布局属性之所以能生效,是因为 Android 会自动在布局文件的最外层再嵌套一个FrameLayout
六、总结
本篇文章重点内容:
1、 LayoutInflater inflate 方法参数的应用,记住下面这个规律:
- 当传入的 root 不为 null 且 attachToRoot 为 false,此时会给 Xml 布局生成的根 View 设置布局参数
- 当传入的 root 不为 null 且 attachToRoot 为 true,此时会将 Xml 布局生成的根 View 通过 addView 方法携带布局参数添加到 root 中
- 当传入的 root 为 null ,此时会将 Xml 布局生成的根 View 对象直接返回
2、Activity 中布局根 View 的布局属性会生效是因为 Android 会自动在布局文件的最外层再嵌套一个 FrameLayout