首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >从一个Bug深入WindowInsets 的传递机制演化

从一个Bug深入WindowInsets 的传递机制演化

作者头像
用户11683348
修改2025-06-18 09:01:02
修改2025-06-18 09:01:02
2000
举报

前言:

Android低版本中的 WindowInsets 分发机制是「懒惰式且缺乏自动性」的 —— 依赖 ViewRootImpl.performTraversals() 调用链的主动触发,无法在布局未完成的场景中保证完整、时机正确地分发 Insets。

从一个Bug开始说起:

今天测试反馈了一个bug,有个项目的某个界面,有一个按钮,应该正常显示的,那个 界面的情况是【刚进入时是沉浸式图片的状态,点击后隐藏所有图标进入全屏状态,再次点击显示图片恢复沉浸式图片的状态】, 但是在某些设备上无法显示,排查后发现,出现这个问题的前提是

  • Android10以下的部分设备,特别是8.0、8.1的设备
  • 从全屏状态恢复沉浸式图片状态后按钮的隐藏和显示正常

看起来很奇怪,但是想起前几天解决的【底部导航栏开启导致toolbar的显示问题】的问题,感觉很可能是系统的问题,有兴趣的可以去看一下这篇文章:

公众号:柿蒂 为什么开启底部导航栏(三大金刚键)后,全屏或沉浸式模式会出现布局异常

先看一下沉浸式图片的代码:

123456789101112131415161718

fun enterImageImmersiveMode(activity: AppCompatActivity, lightStatusBar: Boolean = false) {    val window = activity.window    val controller = WindowInsetsControllerCompat(window, window.decorView)         // 使用post延迟执行以确保UI状态稳定    window.decorView.post {        WindowCompat.setDecorFitsSystemWindows(window, false)        window.statusBarColor = Color.TRANSPARENT        window.navigationBarColor = Color.TRANSPARENT                 controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT        controller.show(WindowInsetsCompat.Type.systemBars())        controller.isAppearanceLightStatusBars = lightStatusBar        controller.isAppearanceLightNavigationBars = lightStatusBar                 activity.supportActionBar?.hide()     }}

为了避免设置的问题特意加上了post,但是还是有问题,而调用下面的代码后,设置isVisible就正常了

123456789101112

fun enterFullscreen(activity: AppCompatActivity) {    val window = activity.window    WindowCompat.setDecorFitsSystemWindows(window, false)    val controller = WindowInsetsControllerCompat(window, window.decorView)    controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE         // 使用post延迟执行以确保UI状态稳定    window.decorView.post {        controller.hide(WindowInsetsCompat.Type.systemBars())        activity.supportActionBar?.hide()    }}

问题本质分析

这是 WindowInsets 与 View 布局生命周期在 Android 8 下处理不一致 导致的,具体包括:

  • WindowCompat.setDecorFitsSystemWindows(window, false) 会关闭系统为内容添加 padding 的行为;
  • Android 8 系统对 WindowInsets 传递不如 Android 10+ 灵活——如果 decorView 尚未完成 layout,系统栏的 insets 会导致布局异常;
  • 我的预设,使用用 post {} 包裹设置逻辑是对的,但在 Android 8 上仍然可能出现 View 不重新 layout 或 requestLayout 无效的情况;
  • 设置 isVisible = true 其实底层调用了 View.setVisibility(View.VISIBLE),但这个 View 如果 layout 未完成或区域为 0x0,它不会显示;
  • 而在后续调用 controller.hide(WindowInsetsCompat.Type.systemBars()) 的时候,触发了一次系统栏动画,从而间接触发了一次 layout 调整 → View 才重新显示出来。

深入剖析:WindowInsets 的传递机制演化

Android版本

WindowInsets分发机制

是否自动触发重新分发

是否支持动态监听Insets变化

Android 4.4 (API 19)

初步引入,使用  fitSystemWindows()

❌ 手动触发

Android 5~8 (API 21–27)

WindowInsets 支持透明状态栏,但依赖  ViewRootImpl 分发

❌(需要手动调用  requestApplyInsets())

Android 9 (API 28)

小幅增强,支持异形屏,但仍需手动触发

✅ 部分事件

Android 10+ (API 29+)

系统主动监听 WindowInsets 变化,并自动分发

✅ 自动触发

✅ 完整支持

所以,在 Android 8 及以前:

  • WindowInsets 分发不是自动的
  • EXNESS外汇官网是:https://one.exnesstrack.org/a/l03oed5fra
  • 只有在 layout 流程中调用 dispatchApplyWindowInsets() 才有效
  • 一旦 View 是 GONE 或尺寸为 0,在 performTraversals() 过程中它不会参与 insets 分发;
  • 导致后续将 View 设置为 VISIBLE 时,它不会自动收到 Insets,即使它应该需要。

深入剖析:为什么 Android 10+ 就不会出这个问题?

从 Android 10(API 29) 开始,谷歌对 WindowInsets 做了核心重构:

改进

说明

View.OnApplyWindowInsetsListener 增强

改为「监听 + 主动分发」机制

ViewRootImpl 改动

引入  WindowInsetsAnimationController 和  InsetsState,用于实时动画支持

DecorView 支持透明栏动画

在 insets 改变时自动 request layout 和重新分发

WindowInsetsController 提供完整动画行为

可以通过滑动触发系统栏显示/隐藏,保证时序正确

所以 Android 10+ 可以保证即使在 View 动态变为 VISIBLE 后,也能重新触发 Insets 传递流程,而 Android 8 不会。

深入剖析:Android 8 源码中的关键限制

我们可以从  Android 8 中的  ViewRootImpl.performTraversals() 看到如下逻辑:

12345

if (mFirst || windowShouldResize || insetsChanged || ...) {    performMeasure();    performLayout();    performDraw();}

其中  insetsChanged 只有在窗口尺寸或类型变化时才会触发,且:

1

dispatchApplyInsets(windowInsets);

只有在 layout 流程中执行,如果 View 在这个阶段是 GONE 或不可见,系统不会再重新分发一次 insets,需要手动:

1

view.requestApplyInsets()  // 或 decorView

倒推bug出现的情况

场景:

  • 在进入沉浸模式后立即设置 View.isVisible = true
  • 这个 View 原本是 GONE,并且尚未 measure/layout
  • Android 8 中不会主动重新调用 dispatchApplyWindowInsets(),导致这个 View 没有 paddingTop(状态栏 inset)

结果:

  • 该 View 的 layout 或 draw 阶段位置错误(比如被导航栏挡住),或者尺寸为 0
  • 会发现 “设置了 VISIBLE,但没显示出来”

解决方案:

初始化沉浸式图片展示状态时手动调用【window.decorView.requestApplyInsets() 】,强制刷新 Insets 和 Layout

1234567891011121314151617181920

fun enterImageImmersiveMode(activity: AppCompatActivity, lightStatusBar: Boolean = false) {    val window = activity.window    val controller = WindowInsetsControllerCompat(window, window.decorView)         // 使用post延迟执行以确保UI状态稳定    window.decorView.post {        WindowCompat.setDecorFitsSystemWindows(window, false)        window.statusBarColor = Color.TRANSPARENT        window.navigationBarColor = Color.TRANSPARENT                 controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT        controller.show(WindowInsetsCompat.Type.systemBars())        controller.isAppearanceLightStatusBars = lightStatusBar        controller.isAppearanceLightNavigationBars = lightStatusBar                 activity.supportActionBar?.hide()        // 💡 强制刷新 Insets 和 Layout        window.decorView.requestApplyInsets()       }}

本文系转载,前往查看

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

本文系转载前往查看

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档