从解决一个分享图片生成的历史bug出发,详解LayoutInflater和View.post的工作原理

问题背景

最近在项目中遇到一个问题:在档口分享功能中,需要动态生成一个分享图片。代码是这样写的:

// 项目中的代码
val shareView = LayoutInflater.from(this@StallMainActivityV1).inflate(R.layout.share_header_stall_main_layout, null)

这个写法本身是正确的,但是在自定义的 AvatarView 中,头像加载的代码执行不到:

iv_avatar.post {// 这里的代码执行不到!val w = if (iv_avatar.width > 0) iv_avatar.width else sizeval h = if (iv_avatar.height > 0) iv_avatar.height else sizeGlideKHelper.loadImageToBitmap(...) { ... }
}

LayoutInflater.inflate() 方法

基本语法

LayoutInflater.inflate() 最常用的重载方法是:

inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View

其中的attchToRoot缺省情况下:

root != null → attachToRoot = true
root == null → attachToRoot = false

参数详解

1. resource (Int) - 布局资源ID

这个没什么好说的,就是你要加载的XML布局文件的资源ID,比如 R.layout.my_layout

2. root (ViewGroup?) - 父容器

这个参数很关键,很多人容易搞错:

  • 可以传 null:创建一个独立的View
  • 可以传具体的ViewGroup:为新创建的View提供LayoutParams参数

重点来了:这个参数的作用主要是为了让新创建的View知道自己的LayoutParams应该是什么样的。

3. attachToRoot (Boolean) - 是否立即添加到父容器
  • true:立即将新View添加到root中,返回的是root
  • false:不添加到root中,但使用root的LayoutParams,返回的是新创建的View
总结
root参数attachToRoot返回值说明
nullfalse(默认)布局文件根View独立View,使用XML中定义的LayoutParams
ViewGroupfalse布局文件根View独立View,但使用parent的LayoutParams
ViewGrouptrue(默认)parent布局文件根View已添加到parent中

常见的几种用法

用法1:创建独立View(分享、Dialog等场景)
val view = LayoutInflater.from(context).inflate(R.layout.my_layout, null)

适用场景:分享图片生成、PopupWindow、Dialog等不需要添加到现有布局的场景。

用法2:为RecyclerView创建ViewHolder
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)

为什么要传parent:让item知道自己应该用什么样的LayoutParams。
为什么是false:因为RecyclerView会自己管理添加时机。

用法3:直接添加到父容器
val view = LayoutInflater.from(context).inflate(R.layout.child_layout, parentView, true)

注意:这种情况下返回的是parentView,不是新创建的View!

我们项目中的用法

项目中的代码:

val shareView = LayoutInflater.from(this@StallMainActivityV1).inflate(R.layout.share_header_stall_main_layout, null)

这个写法是正确的

  • 传入 null 作为parent参数
  • 创建了一个独立的View,适合分享图片生成场景
  • 符合分享场景的使用规范

但是,这样的写法虽然正确,却引出了另一个问题:View.post() 执行不到。

View.post() 工作原理详解

View.post() 是干什么的?

简单来说,View.post() 就是把一个任务扔到主线程的消息队列里,等合适的时候再执行。

执行条件

View.post() 要正常工作,需要满足几个条件:

  1. View必须attached to window(附加到窗口)
  2. View必须在主线程的消息队列中
  3. View必须有有效的Handler

为什么分享场景下执行不到?

在我们的分享场景中:

val shareView = LayoutInflater.from(context).inflate(R.layout.share_header_stall_main_layout, null)// shareView没有被添加到任何父容器中!
// 所以它没有attached to window
// 因此View.post()不会执行

关键点:这个 shareView 只是一个孤立的View对象,它没有被添加到Activity的视图层次结构中。

View的生命周期

要理解这个问题,需要了解View的生命周期:

  1. 创建:通过LayoutInflater创建View对象
  2. 测量:measure() - 确定View的大小
  3. 布局:layout() - 确定View的位置
  4. 绘制:draw() - 把View画出来

只有当View被添加到视图层次结构中时,才会经历完整的生命周期。

当前生命周期状态

在上述分享场景中,通过 LayoutInflater.inflate() 创建的 shareView 仅仅完成了 创建 阶段:

  1. 创建: View对象已通过 LayoutInflater.inflate() 实例化
  2. 测量: 由于没有添加到视图层次结构中,measure()未执行,width/height都是0
  3. 布局: 没有父容器,layout()未执行,位置未确定
  4. 绘制: 没有进入视图层次结构,draw()未执行,无法显示

这就是为什么此时调用 View.post() 会失效 - View还处于"孤立"状态,没有进入完整的生命周期流程。只有将View添加到Activity的视图层次结构中(比如通过 addView() 方法),才会触发后续的测量、布局和绘制过程。

实际的问题表现

StallAvatarView 中:

iv_avatar.post {// 这里执行不到的原因:// 1. iv_avatar没有attached to window// 2. iv_avatar.width 和 iv_avatar.height 都是0val w = if (iv_avatar.width > 0) iv_avatar.width else sizeval h = if (iv_avatar.height > 0) iv_avatar.height else size// ...
}

解决方案

方案1:检查View状态(推荐)

使用isAttachedToWindow,也是我最后修复这个历史问题所使用的解决方案:

isAttachedToWindow 的工作原理

isAttachedToWindow 是 View 类中的一个属性,用于判断当前 View 是否已经被添加到视图层次结构中。它的工作原理如下:

1. 状态变化时机
  • 当 View 通过 addView() 等方法被添加到 Window 时,会调用 onAttachedToWindow(),此时 isAttachedToWindow = true
  • 当 View 通过 removeView() 等方法从 Window 中移除时,会调用 onDetachedFromWindow(),此时 isAttachedToWindow = false
2. 生命周期流程

View 的生命周期状态转换过程如下:

  1. View 对象创建后,初始状态 isAttachedToWindow = false
  2. 调用 Activity.setContentView()ViewGroup.addView() 时:
    • 触发 onAttachedToWindow()
    • 设置 isAttachedToWindow = true
    • 开始 measure、layout、draw 等生命周期
  3. 调用 ViewGroup.removeView() 或 Activity 销毁时:
    • 触发 onDetachedFromWindow()
    • 设置 isAttachedToWindow = false
    • 停止生命周期,释放资源
3. 实际应用价值

检查 isAttachedToWindow 的主要作用:

  1. 避免无效操作 - 在 View 未添加到窗口时,很多操作(如 post())都无法正常执行
  2. 判断时机 - 可以用来判断是否可以进行需要 View 完成布局后才能执行的操作
  3. 防止内存泄漏 - 在 onDetachedFromWindow() 时及时释放资源
  4. 控制生命周期 - 自定义 View 时在合适的时机执行初始化和清理
因此可以这样调整代码:
  • 在进行 View 相关异步操作前,先检查 isAttachedToWindow 状态
  • 对于未 attached 的情况,可以采用备选方案(如使用预设的默认值)
fun showAvatarOrFirstChar(supply_avatar: String,supply_name: String,// ... 其他参数avatarComplete: (() -> Unit)? = null
) {// ... 前面的代码if (supply_avatar.isBlank()) {// 处理无头像情况avatarComplete?.invoke()} else {// 设置View状态tv_avatar.hide()iv_avatar.view()// 关键:检查View是否已经attachedif (isAttachedToWindow) {// 正常情况:View已经在视图层次结构中iv_avatar.post {val w = if (iv_avatar.width > 0) iv_avatar.width else sizeval h = if (iv_avatar.height > 0) iv_avatar.height else sizeloadAvatar(supply_avatar, w, h, avatarComplete)}} else {// 特殊情况:View还没有attached(比如分享场景)// 直接使用传入的size参数loadAvatar(supply_avatar, size, size, avatarComplete)}}
}private fun loadAvatar(supply_avatar: String,width: Int,height: Int,avatarComplete: (() -> Unit)?
) {GlideKHelper.loadImageToBitmap(context, supply_avatar,R.drawable.shape_circle_solid_f0f0f0,width, height) { bmp ->iv_avatar.setImageBitmap(bmp)avatarComplete?.invoke()}
}

方案2:手动测量View

可以设置为MeasureSpec.UNSPECIFIED之后,手动测量
在Android中,View.MeasureSpec.UNSPECIFIED的作用是告诉View它可以按照自己的意愿设置大小,不受任何限制。这是因为:

  1. MeasureSpec的组成
    MeasureSpec是一个32位的整型值,高2位表示测量模式(mode),低30位表示测量大小(size)。测量模式有三种:
  • UNSPECIFIED(0): 父容器不对View进行任何限制,要多大给多大
  • EXACTLY(1): 父容器已经检测出View所需要的精确大小
  • AT_MOST(2): 父容器指定了一个最大值,View的大小不能超过这个值

默认情况下,测量模式取决于View的LayoutParams和父容器的MeasureSpec:

  1. 对于match_parent
  • 父容器是EXACTLY: 子View也是EXACTLY,大小为父容器剩余空间
  • 父容器是AT_MOST: 子View是AT_MOST,最大值为父容器剩余空间
  1. 对于wrap_content
  • 父容器是EXACTLY/AT_MOST: 子View是AT_MOST,最大值为父容器剩余空间
  • 父容器是UNSPECIFIED: 子View也是UNSPECIFIED
  1. 对于具体数值(如100dp)
  • 不管父容器是什么模式,子View都是EXACTLY,大小为指定值
  1. 为什么使用UNSPECIFIED
    当我们手动测量一个未添加到视图层级的View时,使用UNSPECIFIED是最合适的,因为:
  • View此时没有父容器,不需要考虑父容器的限制
  • 让View按照自己的wrap_content逻辑来计算实际需要的尺寸
  • 避免其他模式可能带来的尺寸限制
  1. 测量过程
    当使用UNSPECIFIED时:
  • View会根据自己的内容大小来决定测量结果
  • 对于ViewGroup,它会递归测量所有子View
  • 最终得到的measuredWidth和measuredHeight就是View真实需要的尺寸
  1. 实际应用
    在分享图片生成等场景下,我们需要提前知道View的尺寸,此时使用UNSPECIFIED测量是最佳选择:
// 对于没有attached的View,手动触发测量和布局
val measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
shareView.measure(measureSpec, measureSpec)
shareView.layout(0, 0, shareView.measuredWidth, shareView.measuredHeight)// 现在可以获取到正确的尺寸了

方案3:使用ViewTreeObserver

ViewTreeObserver.OnGlobalLayoutListener 是一个非常有用的回调接口,它会在布局发生变化时被触发。具体来说,在以下情况下会触发:(不过在目前的业务环境下,使用这个回调接口不太符合。)

  1. View的尺寸发生变化
  • View的宽高改变
  • View的padding改变
  • View的margin改变
  1. View的位置发生变化
  • View在父容器中的位置改变
  • View的translation属性改变
  • View的scroll位置改变
  1. View层级发生变化
  • 添加或删除子View
  • View的可见性改变(VISIBLE/GONE/INVISIBLE)
  1. 特殊时机
  • Activity/Fragment首次布局完成
  • 软键盘弹出或收起
  • 屏幕旋转
  • 系统窗口(如状态栏)显示或隐藏

需要注意的是:

  • 一个布局变化可能会触发多次回调
  • 建议在获取到需要的信息后立即移除监听器
  • 不要在回调中执行耗时操作
  • 如果View已被移除,回调可能不会触发

示例代码:

shareView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {override fun onGlobalLayout() {shareView.viewTreeObserver.removeOnGlobalLayoutListener(this)// 现在布局完成了,可以安全地获取View尺寸}
})

总结

LayoutInflater.inflate() 使用建议

  1. 分享、Dialog场景:传 null 作为parent
  2. RecyclerView ViewHolder:传 parent, false
  3. 直接添加到容器:传 parent, true

View.post() 使用建议

  1. 确保View已经attached:使用 isAttachedToWindow 检查
  2. 分享等特殊场景:考虑直接执行,不使用post
  3. 需要View尺寸时:确保View已经经过测量和布局

最佳实践

// ✅ 正确的分享View创建方式
val shareView = LayoutInflater.from(context).inflate(R.layout.share_layout, null)// ✅ 安全的View.post使用方式
if (view.isAttachedToWindow) {view.post { /* 执行需要View尺寸的操作 */ }
} else {// 直接执行或使用其他方式
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/82771.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2.linux目录切换命令:cd与pwd以及路径与路径符

cd 切换当前工作目录 cd [linux路径0] cd没有选项,直接执行,只有参数.如果没有参数,表示回到用户的home目录 pwd 无参,无选项,直接打印当前工作目录的绝对路径 路径 相对路径 以当前目录为起点,路径描述无需使用/开头 # cd Desktop 绝对路径 路径描述需要以/开头 cd…

摩尔条纹 原理以及matlab 实现

一、简介 莫尔条纹的形成原理-CSDN博客 “莫尔”一词源于法文“Moire”,其原本的含义是“波动”或者“起波纹的”。早在古代时期,人们便偶然发现,当把两块薄的丝绸织物相互叠加放置时,能够看到一种呈现不规则形态的花纹。此后&a…

【海康USB相机被HALCON助手连接过后,MVS显示无法连接故障。】

在Halcon里使用助手调用海康USB相机时,如果这个界面点击了【是】 那么恭喜你,相机只能被HALCON调用使用,使用MVS或者海康开发库,将查找不到相机 解决方式: 右键桌面【此电脑】图标 ->选择【管理】 ->选择【设备…

数据治理是什么意思?数据治理平台有哪些?

目录 一、数据治理的概念 1. 数据治理的定义 2. 数据治理的目标 二、数据治理的实施流程 1. 规划阶段 2. 评估阶段 3. 执行阶段 4. 监控与评估阶段 三、常见的数据治理平台 1. FineDataLink 2. IBM InfoSphere Information Governance Catalog 四、总结 随着企业业…

高效工具-tldr

喜欢使用命令操作的小伙伴,肯定会遇到一个问题,查看命令如何使用时,会列出一堆,特别是英文,看的直发懵。前段时间我也是研究git命令,也遇到了类似的问题。好在有大数据,帮我普及相关的知识。 在…

安卓添加设备节点权限和selinux访问权限

# 1 修改设备节点权限及配置属性设置节点值 ## 1.1 修改设备节点权限 ### 1.1.1 不会手动卸载的节点 在system/core/rootdir/init.rc中添加节点权限 在on boot下面添加 chown system system /sys/kernel/usb/host chmod 0664 /sys/kernel/usb/host ### 1.1.2 支持热插拔的…

ssm学习笔记(尚硅谷) day1

创建新项目 maven的聚合 1. 标记父类项目 标签<packaging>pom</packaging>表示将该项目标记为父类项目&#xff0c;必须添加。 以下是标签<packing>的常见取值 groupId在pom.xml中&#xff0c;可以从pom.xml直接修改。 2. 通过<modules>添加子项目…

基于Java,SpringBoot,Vue,UniAPP医院预约挂号买药就诊病例微信小程序系统设计

摘要 随着医疗信息化的不断推进以及“互联网医疗”模式的广泛普及&#xff0c;传统医院挂号流程中存在的排队时间长、资源分配不均等问题日益凸显&#xff0c;急需通过数字化手段加以解决。本研究设计并实现了一套基于Java、SpringBoot、Vue与UniAPP技术栈的医院预约挂号微信小…

Axure项目实战:运输统计页引入echarts实现高保真设计(JS代码ctrl+c ctrl+v懂得来)

亲爱的小伙伴,在您浏览之前,烦请关注一下,在此深表感谢!如有帮助请订阅专栏! Axure产品经理精品视频课已登录CSDN可点击学习https://edu.csdn.net/course/detail/40420 案例视频: 数据统计引入echarts示例演示 课程主题:运输统计页引入echarts实现高保真设计 主要内容…

python打卡day39

图像数据与显存 知识点回顾 图像数据的格式&#xff1a;灰度和彩色数据模型的定义显存占用的4种地方 模型参数梯度参数优化器参数数据批量所占显存神经元输出中间状态 batchisize和训练的关系 作业&#xff1a;今日代码较少&#xff0c;理解内容即可 在 PyTorch 中&#xff0c;…

15.1 【基础项目】使用 HTML、CSS 和 TypeScript 构建的简单计数器应用

一个简单的计数器应用是学习如何集成 HTML、CSS 和 TypeScript 的绝佳项目。该应用允许用户对计数值进行增加、减少和重置&#xff0c;展示了 TypeScript 中基本的 DOM 操作和事件处理。 我们将构建的内容 我们将创建一个具有以下功能的计数器应用&#xff1a; 增加计数值减…

RT-Thread源码阅读(3)——内核对象管理

_object_container对象容器数组 在RT-Thread操作系统中&#xff0c;_object_container数组的作用是按类型分类管理内核对象&#xff0c;提供高效的类型检查、资源管理和统计功能 struct rt_list_node {struct rt_list_node *next; /**< point to…

《智能医学》征稿通知:7天可见刊,专科及以上可发表

香港科学出版社(Hong Kong Scientific Publishers Journals)是一家全球独立高质量的学术出版机构&#xff0c;遵循国际开放获取的出版(OA)原则。现已与科检易学术携手共同征集高质量文章。目前可出版来自高等学校、科研院所和企业的先进科技成果。包括理、工、农、医、经、管、…

如何利用categraf的exec插件实现对Linux主机系统用户及密码有效期进行监控及告警?

需求描述 Categraf作为夜莺监控平台的数据采集工具&#xff0c;为了保障Linux主机的安全&#xff0c;需要实现对系统用户密码有效期的监控&#xff0c;并在密码即将到期时及时告警&#xff0c;以提醒运维人员更改密码。本章将详细介绍如何利用Categraf的exec插件来实现这一功能…

RV1126-OPENCV 交叉编译

一.下载opencv-3.4.16.zip到自己想装的目录下 二.解压并且打开 opencv 目录 先用 unzip opencv-3.4.16.zip 来解压 opencv 的压缩包&#xff0c;并且进入 opencv 目录(cd opencv-3.4.16) 三. 修改 opencv 的 cmake 脚本的内容 先 cd platforms/linux 然后修改 arm-gnueabi.to…

如何加载私钥为 SecKeyRef

本文介绍如何在 iOS/macOS 下将私钥加载为 SecKeyRef&#xff0c;涵盖 PEM 格式的 ECC 密钥读取、X9.63 数据构建、以及与 Keychain 的集成。 1. 使用 SecKeyCreateWithData 加载私钥 Apple 提供的 SecKeyCreateWithData 方法可以直接将密钥数据加载为 SecKeyRef 对象。 SecK…

Missashe考研日记—Day44-Day50

Missashe考研日记—Day44-Day50 写在面前 本系列博客用于记录博主一周的学习进度&#xff0c;具体知识总结在目前已有的笔记中&#xff1a;1.高数强化学习笔记2.计网复习笔记3.新增&#xff1a;线代题型总结 专业课408 这周先是把计网第三章数据链路层剩下的局域网以及之后…

Windows下安装并使用kubectl查看K8S日志

【1】安装kubectl 官网文档&#xff1a;https://kubernetes.io/zh-cn/docs/tasks/tools/install-kubectl-windows/ 下载后得到 kubectl.exe&#xff0c;放到一个目录下&#xff0c;然后配置环境变量。 此时CMD 进入DOS命令窗口 kubectl version【2】配置config文件 其实就是…

攻防世界János-the-Ripper

打开压缩包是一个文件&#xff0c;用010Editor打开可以发现里面有隐藏文件flag.txt 此时想到分离文件&#xff0c;利用binwalk工具 利用binwalk生成出的是一个压缩包&#xff0c;解压缩但是发现竟然解压需要密码 这里就可以开始暴力破解密码了&#xff0c;这里我用的是ARCHPR工…