前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >玩转LayoutInflater

玩转LayoutInflater

作者头像
Rouse
发布2021-07-08 14:50:53
4440
发布2021-07-08 14:50:53
举报
文章被收录于专栏:Android补给站Android补给站

作者:sweetying 链接:https://juejin.cn/post/6976613726036656159

前言

今天准备和大家分享的是 LayoutInflater,我给它取名:布局打气筒,很形象,其实就是根据英文翻译过来的?。我们知道气球打气筒可以给气球打气从而改变它的形状。而布局打气筒的作用就是给我们的 Xml 布局打气让它变成一个个 View 对象。在我们的日常工作中,经常会接触到他,因为只要你写了 Xml 布局,你就要使用 LayoutInflater,下面我们就来好好讲讲它。

注意:本文所展示的系统源码都是基于Android-30 ,并提取核心部分进行分析

基本使用

LayoutInflater 实例获取

  1. 通过 LayoutInflater 的静态方法 from 获取
  2. 通过系统服务 getSystemService 方法获取
  3. 如果是在 Activity 或 Fragment 可直接获取到实例
代码语言:javascript
复制
//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 对应的布局如下:
代码语言:javascript
复制
<?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"/>
  1. 创建一个新的布局取名 item_main.xml,如下图:
  1. 修改 MainActivity 中的代码
代码语言:javascript
复制
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 中的代码,如下:

代码语言:javascript
复制
val itemMain = layoutInflater.inflate(R.layout.item_main, consMain)
//等同下面这行代码
val itemMain = layoutInflater.inflate(R.layout.item_main, consMain,true)

实际上上面这句代码就相当于调用了三个参数的重载方法,且第三个参数为 true,我们看下它两个参数的源码:

代码语言:javascript
复制
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}

现在在运行看下结果:

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

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

代码语言:javascript
复制
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 个重载方法,看下它的源码,仅贴出关键代码:

代码语言:javascript
复制
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 重载方法:

代码语言:javascript
复制
inflate(@LayoutRes int resource, @Nullable ViewGroup root)

传入的实参:resouce 传入了一个 Xml 布局,root 传入了 null

根据我们上面源码得到的结论,当传入的 root 为 null ,此时会将 Xml 布局生成的根 View 对象直接返回

那么此时这个布局根 View 不在任何 View 中,因此它的布局属性失效了,但是 TextView 在一个布局中,它的布局属性会生效,因此就出现了上述截图中的效果

  1. 问题 2

上述问题 2 我们调用的还是 LayoutInflater 两个参数的构造方法

传入的实参:resouce 传入了一个 Xml 布局,root 传入了 consMain

实际又会调用 LayoutInflater 三个参数的 inflate 重载方法:

代码语言:javascript
复制
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 去掉就可以了

  1. 预期效果

上述预期效果,我们调用的是 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 对象直接返回
  1. Activity 中布局根 View 的布局属性会生效是因为 Android 会自动在布局文件的最外层再嵌套一个 FrameLayout

好了,本篇文章到这里就结束了,希望能给你带来帮助

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android补给站 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 基本使用
    • LayoutInflater 实例获取
    • 例子
    • LayoutInflater inflate 系列方法源码分析
    • 问题分析
    • 为啥 Activity 中布局根 View 的布局属性会生效?
    • 总结
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档