注册

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 系列方法了,这几个方法是本篇文章的一个重点,如下:

image-20210622163719911

从 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,如下图:

image-20210622174620878

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 中,运行看下效果:

image-20210622175552693

啥情况?怎么和预想的不一样呢?我的背景颜色怎么不见了?把这个问题 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);
}

现在在运行看下结果:

image-20210622190018488

报错了,提示我们当前 child 已经有了一个父 View,你必须先调用父 View 的 removeView 方法移除当前 child 才行。是不是疑问更多了呢?把这个问题 2 也先记着

我们在修改一下 MainActivity 中的代码,如下:

val itemMain = layoutInflater.inflate(R.layout.item_main, consMain,false)

在运行看下结果:

image-20210622190835239

嗯,现在达到了我们预期的效果

现在回到上面那两个问题,分析发现是 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 页面组成,这里以常见页面为例

image-20210622210219600

我们的页面中有一个顶级 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

0 个评论

要回复文章请先登录注册