区块链技术博客
www.b2bchain.cn

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)求职学习资料

本文介绍了Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)求职学习资料,有助于帮助完成毕业设计以及求职,是一篇很好的资料。

对技术面试,学习经验等有一些体会,在此分享。

前言

很高兴见到你!👋

作为 Android 开发者,不知道你是否遇到这样的问题:

  • 不知道如何获取状态栏,导航栏以及软键盘高度

  • 将内容绘制到状态栏和导航栏区域后发生视觉和交互冲突

  • 不懂适配 edge-to-edge 的原理

  • 搞不清 android:fitsSystemWindows 这个属性的作用

9 月份纯纯写作的开发者 Drakeet 大佬在扔物线的知识星球中分享了关于 WindowInsets 的视频内容,视频介绍了以下内容:

  • WindowInsets 是什么?
  • WindowInsets 如何被分发?
  • 如何适配透明手势导航栏(edge-to-edge)?
  • 如何准确监听软键盘弹出和获得软键盘高度?
  • 如何自定义软键盘弹出后的响应行为?

本文是对该视频的补充,内容大多摘自 Chris Banes 在 2017 年做的演讲:Becoming a master window fitter🔧(已搬运至B站)。

阅读本文,你将了解以下内容:

  • System bar 各个版本能力的变化
  • App 将内容绘制到 Status barNavigation bar 后面的原理
  • fitsSystemWindowsWindowInsets 的概念
  • WindowInsets 的分发逻辑
  • 处理 WindowInsets 的最佳实践

限于篇幅原因,本文没有太多分析源码。关于底层的实现原理,我们在之后的文章中介绍。仅关心最佳实践的小伙伴可以直接跳转最佳实践一节。

让我们开始吧~

什么是Window

在 Android Detail:Window 篇——站在 Window 视角理解 Activity 任务与返回栈一文中我们讨论过 Android Window 的核心概念并得到一个结论:

在 Android 中,暴露给开发者操作 UI 界面的 API 是 mWindowManager.addView(rootView, windowParams);

简单说,Android 屏幕上的每一个 view 都是在 Window 内的

  • 每个 Activity 有着自己的 Window(PhoneWindow),Activity#getWindow()
  • Dialog 也有自己的 Window,Dialog#getWindow()
  • PopupWindow,Toast 也是通过 WindowManager#addView 将 view 置于 Widnow 上的

什么是 Insets

屏幕上除了开发者 app 绘制的内容还有系统的 Insets(插入物),Insets 区域负责描述屏幕的哪些部分会与系统 UI 相交。如 Starus barNavigation bar

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Insets

常见的 Insets 有:

  • STATUS_BAR,用于展示系统时间,电量,wifi 等信息
  • NAVIGATION_BAR,虚拟导航栏(区别于实体的三大金刚键),形态有三大金刚键导航,手势导航两种。(有些设备形态如 TV 没有导航栏)
  • IME,软键盘,用于输入文字

其中 STATUS_BARNAVIGATION_BAR 又被称为 System bar

如果开发者绘制的内容出现在了系统 UI 区域内,就可能出现视觉与手势的冲突。开发者可以借助 Insets 把 view 从屏幕边缘向内移动到一个合适的位置。

在源码中,Insets 对象拥有 4 个 int 值,用于描述矩形四个边的偏移:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

insets.drawio

📢 注意:不要把 Insets 的 topbottomleftright 与 Rect 的搞混,前者描述的是偏移,后者是坐标

关于 Insets 更详尽的信息,可以 查看这篇文章。

setSystemUiVisibility 与 WTFs

View 的源码中有一个 setSystemUiVisibility() 的方法,虽然该方法在 Android 11 已被弃用,但按照本专栏的一贯风格,我们还是要来介绍一下该方法。

有些场景开发者可能希望 app 的内容可以绘制到状态栏或导航栏的区域以提供更好的用户体验,因此系统提供了 setSystemUiVisibility 方法,开发者可以通过向该方法传入不同的 flag 以应对不同的使用场景。

这些 flag 被称为 Window Transform Flags,简称 WTFs(滑稽脸😏),同样的,它们在 Android 11 中被弃用。常用的 flag 如下:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

如果想了解这些 flag 的效果可以 移步这里。

System bar 能力变化史

Android 4.4 之前

内容显示在 System bar 之间,即下图红框所在区域:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

开发者可以使用 setSystemUiVisibility 方法将内容绘制到状态栏后面,下图红框区域:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 4.4

Android 4.4 引入了 android:windowTranslucentStatusandroid:windowTranslucentNavigation ,允许开发者将 System bar 设置成透明:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

System bar background 是由 WindowManager 绘制的(利用 Window 的 flag)

Android 5.0

之前版本 System bar 都是由 WindowManager 绘制的,在 Android 5.0,引入了 android:windowDrawsSystemBarBackgrounds,当 windowDrawsSystemBarBackgrounds 为 true(默认值) 时,System bar 的 background 在 Window 内部。如下图:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

system bar 在 Window 内

开发者可以调用 Window 的方法为 System bar 设置颜色:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

设置状态栏颜色

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

设置导航栏颜色

📢注意windowTranslucentStatuswindowTranslucentNavigation 要比为 System bar 设置自定义颜色的优先级更高

windowTranslucentStatuswindowTranslucentNavigation 设置为 true 后会导致 windowDrawsSystemBarBackgroundsfalseSystem bar background 由 WindowManager 接管。

自 Android 5.0 后,当 windowDrawsSystemBarBackgrounds 为 true 时,System bar 作为 window 的一部分。换言之,DecorView(FrameLayout 子类)有三个子 View:显示 App 内容的 LinearLayout 以及 Status barNavigation bar

默认情况下,App 的内容显示在 System bar 中间。

理论上,显示 App 内容的 LinearLayout 应该充满屏幕,系统使用了 paddingTop 和 marginBottom 为 System bar 预留出了空间

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

那么 App 的内容区域是如何绘制到 System bar 后面的?很简单,LinearLayout 没有 padding 和 margin(我们在后文介绍原理),充满屏幕:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 10

随着全面屏设备的普及,越来越多的 Android 设备突破了 16:9 的限制,Android 10 推出了新的导航模式:手势导航。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

屏幕边缘左滑和右滑可以触发 Back,底部向上轻扫可以触发 Home

新的手势导航与原来的三大金刚键的 Navigation bar 一样,只不过高度变小了。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

新的手势导航

如果 Navigation bar 是透明的,底部的「小白条」是可以跟随背景动态改变颜色的(与 iOS 一样,不知道谁抄的谁🤣)

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 11

Android 11 引入了 WindowInsetsAnimation 允许监听 Insets 的变化进度,使用户体验更加丝滑。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 11 WindowInsets 动画

小结

为了方便开发者更合理地使用设备屏幕绘制内容,Android 在历代版本不断迭代 System bar 控制的 API,功能越来越完善。

当开发者将 App 内容绘制到 System bar 后面时要考虑视觉冲突和手势冲突。

为了防止 App 内容区域与 System bar 发生视觉冲突,官方提供了两种 API, WidowInsetsfitsSystemWindows

WindowInsets

WindowInsets 描述了一组 Window Content 的 Insets,未来可能会继续添加新的 Insets 类型。目前已有的 Insets 类型有:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Insets type

使用位运算管理状态是很常见并高效的方式,如果对这部分内容不了解,可以移步 KunMinX 的 这篇文章 入门

System bar 包括 Status barNavigation barCaption bar,但不包括软键盘ime

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

onApplyWindowInsets 与 setOnApplyWindowInsetsListener

开发者可以通过在自定义 View 中重写 onApplyWindowInsets() 方法或调用 setOnApplyWindowInsetsListener() 来监听 WindowInsets 的变化,通过对 View 添加 marginpadding 的方式处理解决冲突

这两个方法是互斥的,当存在 OnApplyWindowInsetsListener 时不会执行 onApplyWindowInsets

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

不过开发者可以在也可 OnApplyWindowInsetsListener 手动调用 onApplyWindowInsets 使两个方法同时被执行。

WindowInsets 分发

前文我们提到,如果开发者绘制的内容出现在了系统 UI 区域内,就可能出现视觉与手势的冲突。开发者可以借助 Insets 把 view 从屏幕边缘向内移动到一个合适的位置,此时 View#onApplyWindowInsets() 会被调用。那么这些 Insets 是如何分发给 View 的呢?

笔者在 View 事件分发机制,大型职场 PUA 现场 一文中把 Android 的视图树抽象为 N 叉树

与 View 的事件分发一样,WindowInsets 的分发也是 N 叉树的遍历过程:

从 N 叉树的根节点(DecoView)开始,按照 深度优先 的方式分发给 子 view。

Android 10 和 Android 11 两个版本官方连续修改了 ViewGroup#dispatchApplyWindowInsets() 的逻辑(具体我们在源码解析篇介绍)。

如果 app targetSdkVersion < 30 ,如果某个节点消费了 Insets,所有没遍历到的节点都不会收到 WindowInsets 的分发;

当 app 运行在 Android 11 以上版本的设备上且 targetSdkVersion >= 30,如果某个节点消费了 Insets,该节点的所有子节点不会收到 WindowInsets 分发。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

WindowInsets 分发

旧版本的分发有一个问题,无法做到两个同级的 View 同时消费 WindowInsets,如下图:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

我们可以将 Level2-1 和 Level2-2 看成顶部导航和底部导航,按照旧逻辑,当 Level2-1 消费了 WindowInsets,另一个 View 便没机会了。

小结

  • 由于开发者可以将 App 内容绘制到与系统 UI 相交的位置,因此官方为开发者提供了解决视觉冲突的方式,WindowInsets
  • 开发者可以重写 View#onApplyWindowInsetsView#setOnApplyWindowInsetsListener 来根据 WindowInsets 对系统 UI 进行位置避让(对 view 设置 padding 或 margin)。
  • 下一节介绍的 fitsSystemWindows 的默认行为也是通过 onApplyWindowInsets 实现的。

前言

很高兴见到你!👋

作为 Android 开发者,不知道你是否遇到这样的问题:

  • 不知道如何获取状态栏,导航栏以及软键盘高度

  • 将内容绘制到状态栏和导航栏区域后发生视觉和交互冲突

  • 不懂适配 edge-to-edge 的原理

  • 搞不清 android:fitsSystemWindows 这个属性的作用

9 月份纯纯写作的开发者 Drakeet 大佬在扔物线的知识星球中分享了关于 WindowInsets 的视频内容,视频介绍了以下内容:

  • WindowInsets 是什么?
  • WindowInsets 如何被分发?
  • 如何适配透明手势导航栏(edge-to-edge)?
  • 如何准确监听软键盘弹出和获得软键盘高度?
  • 如何自定义软键盘弹出后的响应行为?

本文是对该视频的补充,内容大多摘自 Chris Banes 在 2017 年做的演讲:Becoming a master window fitter🔧(已搬运至B站)。

阅读本文,你将了解以下内容:

  • System bar 各个版本能力的变化
  • App 将内容绘制到 Status barNavigation bar 后面的原理
  • fitsSystemWindowsWindowInsets 的概念
  • WindowInsets 的分发逻辑
  • 处理 WindowInsets 的最佳实践

限于篇幅原因,本文没有太多分析源码。关于底层的实现原理,我们在之后的文章中介绍。仅关心最佳实践的小伙伴可以直接跳转最佳实践一节。

让我们开始吧~

什么是Window

在 Android Detail:Window 篇——站在 Window 视角理解 Activity 任务与返回栈一文中我们讨论过 Android Window 的核心概念并得到一个结论:

在 Android 中,暴露给开发者操作 UI 界面的 API 是 mWindowManager.addView(rootView, windowParams);

简单说,Android 屏幕上的每一个 view 都是在 Window 内的

  • 每个 Activity 有着自己的 Window(PhoneWindow),Activity#getWindow()
  • Dialog 也有自己的 Window,Dialog#getWindow()
  • PopupWindow,Toast 也是通过 WindowManager#addView 将 view 置于 Widnow 上的

什么是 Insets

屏幕上除了开发者 app 绘制的内容还有系统的 Insets(插入物),Insets 区域负责描述屏幕的哪些部分会与系统 UI 相交。如 Starus barNavigation bar

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Insets

常见的 Insets 有:

  • STATUS_BAR,用于展示系统时间,电量,wifi 等信息
  • NAVIGATION_BAR,虚拟导航栏(区别于实体的三大金刚键),形态有三大金刚键导航,手势导航两种。(有些设备形态如 TV 没有导航栏)
  • IME,软键盘,用于输入文字

其中 STATUS_BARNAVIGATION_BAR 又被称为 System bar

如果开发者绘制的内容出现在了系统 UI 区域内,就可能出现视觉与手势的冲突。开发者可以借助 Insets 把 view 从屏幕边缘向内移动到一个合适的位置。

在源码中,Insets 对象拥有 4 个 int 值,用于描述矩形四个边的偏移:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

insets.drawio

📢 注意:不要把 Insets 的 topbottomleftright 与 Rect 的搞混,前者描述的是偏移,后者是坐标

关于 Insets 更详尽的信息,可以 查看这篇文章。

setSystemUiVisibility 与 WTFs

View 的源码中有一个 setSystemUiVisibility() 的方法,虽然该方法在 Android 11 已被弃用,但按照本专栏的一贯风格,我们还是要来介绍一下该方法。

有些场景开发者可能希望 app 的内容可以绘制到状态栏或导航栏的区域以提供更好的用户体验,因此系统提供了 setSystemUiVisibility 方法,开发者可以通过向该方法传入不同的 flag 以应对不同的使用场景。

这些 flag 被称为 Window Transform Flags,简称 WTFs(滑稽脸😏),同样的,它们在 Android 11 中被弃用。常用的 flag 如下:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

如果想了解这些 flag 的效果可以 移步这里。

System bar 能力变化史

Android 4.4 之前

内容显示在 System bar 之间,即下图红框所在区域:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

开发者可以使用 setSystemUiVisibility 方法将内容绘制到状态栏后面,下图红框区域:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 4.4

Android 4.4 引入了 android:windowTranslucentStatusandroid:windowTranslucentNavigation ,允许开发者将 System bar 设置成透明:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

System bar background 是由 WindowManager 绘制的(利用 Window 的 flag)

Android 5.0

之前版本 System bar 都是由 WindowManager 绘制的,在 Android 5.0,引入了 android:windowDrawsSystemBarBackgrounds,当 windowDrawsSystemBarBackgrounds 为 true(默认值) 时,System bar 的 background 在 Window 内部。如下图:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

system bar 在 Window 内

开发者可以调用 Window 的方法为 System bar 设置颜色:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

设置状态栏颜色

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

设置导航栏颜色

📢注意windowTranslucentStatuswindowTranslucentNavigation 要比为 System bar 设置自定义颜色的优先级更高

windowTranslucentStatuswindowTranslucentNavigation 设置为 true 后会导致 windowDrawsSystemBarBackgroundsfalseSystem bar background 由 WindowManager 接管。

自 Android 5.0 后,当 windowDrawsSystemBarBackgrounds 为 true 时,System bar 作为 window 的一部分。换言之,DecorView(FrameLayout 子类)有三个子 View:显示 App 内容的 LinearLayout 以及 Status barNavigation bar

默认情况下,App 的内容显示在 System bar 中间。

理论上,显示 App 内容的 LinearLayout 应该充满屏幕,系统使用了 paddingTop 和 marginBottom 为 System bar 预留出了空间

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

那么 App 的内容区域是如何绘制到 System bar 后面的?很简单,LinearLayout 没有 padding 和 margin(我们在后文介绍原理),充满屏幕:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 10

随着全面屏设备的普及,越来越多的 Android 设备突破了 16:9 的限制,Android 10 推出了新的导航模式:手势导航。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

屏幕边缘左滑和右滑可以触发 Back,底部向上轻扫可以触发 Home

新的手势导航与原来的三大金刚键的 Navigation bar 一样,只不过高度变小了。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

新的手势导航

如果 Navigation bar 是透明的,底部的「小白条」是可以跟随背景动态改变颜色的(与 iOS 一样,不知道谁抄的谁🤣)

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 11

Android 11 引入了 WindowInsetsAnimation 允许监听 Insets 的变化进度,使用户体验更加丝滑。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 11 WindowInsets 动画

小结

为了方便开发者更合理地使用设备屏幕绘制内容,Android 在历代版本不断迭代 System bar 控制的 API,功能越来越完善。

当开发者将 App 内容绘制到 System bar 后面时要考虑视觉冲突和手势冲突。

为了防止 App 内容区域与 System bar 发生视觉冲突,官方提供了两种 API, WidowInsetsfitsSystemWindows

WindowInsets

WindowInsets 描述了一组 Window Content 的 Insets,未来可能会继续添加新的 Insets 类型。目前已有的 Insets 类型有:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Insets type

使用位运算管理状态是很常见并高效的方式,如果对这部分内容不了解,可以移步 KunMinX 的 这篇文章 入门

System bar 包括 Status barNavigation barCaption bar,但不包括软键盘ime

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

onApplyWindowInsets 与 setOnApplyWindowInsetsListener

开发者可以通过在自定义 View 中重写 onApplyWindowInsets() 方法或调用 setOnApplyWindowInsetsListener() 来监听 WindowInsets 的变化,通过对 View 添加 marginpadding 的方式处理解决冲突

这两个方法是互斥的,当存在 OnApplyWindowInsetsListener 时不会执行 onApplyWindowInsets

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

不过开发者可以在也可 OnApplyWindowInsetsListener 手动调用 onApplyWindowInsets 使两个方法同时被执行。

WindowInsets 分发

前文我们提到,如果开发者绘制的内容出现在了系统 UI 区域内,就可能出现视觉与手势的冲突。开发者可以借助 Insets 把 view 从屏幕边缘向内移动到一个合适的位置,此时 View#onApplyWindowInsets() 会被调用。那么这些 Insets 是如何分发给 View 的呢?

笔者在 View 事件分发机制,大型职场 PUA 现场 一文中把 Android 的视图树抽象为 N 叉树

与 View 的事件分发一样,WindowInsets 的分发也是 N 叉树的遍历过程:

从 N 叉树的根节点(DecoView)开始,按照 深度优先 的方式分发给 子 view。

Android 10 和 Android 11 两个版本官方连续修改了 ViewGroup#dispatchApplyWindowInsets() 的逻辑(具体我们在源码解析篇介绍)。

如果 app targetSdkVersion < 30 ,如果某个节点消费了 Insets,所有没遍历到的节点都不会收到 WindowInsets 的分发;

当 app 运行在 Android 11 以上版本的设备上且 targetSdkVersion >= 30,如果某个节点消费了 Insets,该节点的所有子节点不会收到 WindowInsets 分发。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

WindowInsets 分发

旧版本的分发有一个问题,无法做到两个同级的 View 同时消费 WindowInsets,如下图:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

我们可以将 Level2-1 和 Level2-2 看成顶部导航和底部导航,按照旧逻辑,当 Level2-1 消费了 WindowInsets,另一个 View 便没机会了。

小结

  • 由于开发者可以将 App 内容绘制到与系统 UI 相交的位置,因此官方为开发者提供了解决视觉冲突的方式,WindowInsets
  • 开发者可以重写 View#onApplyWindowInsetsView#setOnApplyWindowInsetsListener 来根据 WindowInsets 对系统 UI 进行位置避让(对 view 设置 padding 或 margin)。
  • 下一节介绍的 fitsSystemWindows 的默认行为也是通过 onApplyWindowInsets 实现的。

前言

很高兴见到你!👋

作为 Android 开发者,不知道你是否遇到这样的问题:

  • 不知道如何获取状态栏,导航栏以及软键盘高度

  • 将内容绘制到状态栏和导航栏区域后发生视觉和交互冲突

  • 不懂适配 edge-to-edge 的原理

  • 搞不清 android:fitsSystemWindows 这个属性的作用

9 月份纯纯写作的开发者 Drakeet 大佬在扔物线的知识星球中分享了关于 WindowInsets 的视频内容,视频介绍了以下内容:

  • WindowInsets 是什么?
  • WindowInsets 如何被分发?
  • 如何适配透明手势导航栏(edge-to-edge)?
  • 如何准确监听软键盘弹出和获得软键盘高度?
  • 如何自定义软键盘弹出后的响应行为?

本文是对该视频的补充,内容大多摘自 Chris Banes 在 2017 年做的演讲:Becoming a master window fitter🔧(已搬运至B站)。

阅读本文,你将了解以下内容:

  • System bar 各个版本能力的变化
  • App 将内容绘制到 Status barNavigation bar 后面的原理
  • fitsSystemWindowsWindowInsets 的概念
  • WindowInsets 的分发逻辑
  • 处理 WindowInsets 的最佳实践

限于篇幅原因,本文没有太多分析源码。关于底层的实现原理,我们在之后的文章中介绍。仅关心最佳实践的小伙伴可以直接跳转最佳实践一节。

让我们开始吧~

什么是Window

在 Android Detail:Window 篇——站在 Window 视角理解 Activity 任务与返回栈一文中我们讨论过 Android Window 的核心概念并得到一个结论:

在 Android 中,暴露给开发者操作 UI 界面的 API 是 mWindowManager.addView(rootView, windowParams);

简单说,Android 屏幕上的每一个 view 都是在 Window 内的

  • 每个 Activity 有着自己的 Window(PhoneWindow),Activity#getWindow()
  • Dialog 也有自己的 Window,Dialog#getWindow()
  • PopupWindow,Toast 也是通过 WindowManager#addView 将 view 置于 Widnow 上的

什么是 Insets

屏幕上除了开发者 app 绘制的内容还有系统的 Insets(插入物),Insets 区域负责描述屏幕的哪些部分会与系统 UI 相交。如 Starus barNavigation bar

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Insets

常见的 Insets 有:

  • STATUS_BAR,用于展示系统时间,电量,wifi 等信息
  • NAVIGATION_BAR,虚拟导航栏(区别于实体的三大金刚键),形态有三大金刚键导航,手势导航两种。(有些设备形态如 TV 没有导航栏)
  • IME,软键盘,用于输入文字

其中 STATUS_BARNAVIGATION_BAR 又被称为 System bar

如果开发者绘制的内容出现在了系统 UI 区域内,就可能出现视觉与手势的冲突。开发者可以借助 Insets 把 view 从屏幕边缘向内移动到一个合适的位置。

在源码中,Insets 对象拥有 4 个 int 值,用于描述矩形四个边的偏移:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

insets.drawio

📢 注意:不要把 Insets 的 topbottomleftright 与 Rect 的搞混,前者描述的是偏移,后者是坐标

关于 Insets 更详尽的信息,可以 查看这篇文章。

setSystemUiVisibility 与 WTFs

View 的源码中有一个 setSystemUiVisibility() 的方法,虽然该方法在 Android 11 已被弃用,但按照本专栏的一贯风格,我们还是要来介绍一下该方法。

有些场景开发者可能希望 app 的内容可以绘制到状态栏或导航栏的区域以提供更好的用户体验,因此系统提供了 setSystemUiVisibility 方法,开发者可以通过向该方法传入不同的 flag 以应对不同的使用场景。

这些 flag 被称为 Window Transform Flags,简称 WTFs(滑稽脸😏),同样的,它们在 Android 11 中被弃用。常用的 flag 如下:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

如果想了解这些 flag 的效果可以 移步这里。

System bar 能力变化史

Android 4.4 之前

内容显示在 System bar 之间,即下图红框所在区域:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

开发者可以使用 setSystemUiVisibility 方法将内容绘制到状态栏后面,下图红框区域:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 4.4

Android 4.4 引入了 android:windowTranslucentStatusandroid:windowTranslucentNavigation ,允许开发者将 System bar 设置成透明:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

System bar background 是由 WindowManager 绘制的(利用 Window 的 flag)

Android 5.0

之前版本 System bar 都是由 WindowManager 绘制的,在 Android 5.0,引入了 android:windowDrawsSystemBarBackgrounds,当 windowDrawsSystemBarBackgrounds 为 true(默认值) 时,System bar 的 background 在 Window 内部。如下图:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

system bar 在 Window 内

开发者可以调用 Window 的方法为 System bar 设置颜色:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

设置状态栏颜色

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

设置导航栏颜色

📢注意windowTranslucentStatuswindowTranslucentNavigation 要比为 System bar 设置自定义颜色的优先级更高

windowTranslucentStatuswindowTranslucentNavigation 设置为 true 后会导致 windowDrawsSystemBarBackgroundsfalseSystem bar background 由 WindowManager 接管。

自 Android 5.0 后,当 windowDrawsSystemBarBackgrounds 为 true 时,System bar 作为 window 的一部分。换言之,DecorView(FrameLayout 子类)有三个子 View:显示 App 内容的 LinearLayout 以及 Status barNavigation bar

默认情况下,App 的内容显示在 System bar 中间。

理论上,显示 App 内容的 LinearLayout 应该充满屏幕,系统使用了 paddingTop 和 marginBottom 为 System bar 预留出了空间

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

那么 App 的内容区域是如何绘制到 System bar 后面的?很简单,LinearLayout 没有 padding 和 margin(我们在后文介绍原理),充满屏幕:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 10

随着全面屏设备的普及,越来越多的 Android 设备突破了 16:9 的限制,Android 10 推出了新的导航模式:手势导航。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

屏幕边缘左滑和右滑可以触发 Back,底部向上轻扫可以触发 Home

新的手势导航与原来的三大金刚键的 Navigation bar 一样,只不过高度变小了。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

新的手势导航

如果 Navigation bar 是透明的,底部的「小白条」是可以跟随背景动态改变颜色的(与 iOS 一样,不知道谁抄的谁🤣)

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 11

Android 11 引入了 WindowInsetsAnimation 允许监听 Insets 的变化进度,使用户体验更加丝滑。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Android 11 WindowInsets 动画

小结

为了方便开发者更合理地使用设备屏幕绘制内容,Android 在历代版本不断迭代 System bar 控制的 API,功能越来越完善。

当开发者将 App 内容绘制到 System bar 后面时要考虑视觉冲突和手势冲突。

为了防止 App 内容区域与 System bar 发生视觉冲突,官方提供了两种 API, WidowInsetsfitsSystemWindows

WindowInsets

WindowInsets 描述了一组 Window Content 的 Insets,未来可能会继续添加新的 Insets 类型。目前已有的 Insets 类型有:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

Insets type

使用位运算管理状态是很常见并高效的方式,如果对这部分内容不了解,可以移步 KunMinX 的 这篇文章 入门

System bar 包括 Status barNavigation barCaption bar,但不包括软键盘ime

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

onApplyWindowInsets 与 setOnApplyWindowInsetsListener

开发者可以通过在自定义 View 中重写 onApplyWindowInsets() 方法或调用 setOnApplyWindowInsetsListener() 来监听 WindowInsets 的变化,通过对 View 添加 marginpadding 的方式处理解决冲突

这两个方法是互斥的,当存在 OnApplyWindowInsetsListener 时不会执行 onApplyWindowInsets

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

不过开发者可以在也可 OnApplyWindowInsetsListener 手动调用 onApplyWindowInsets 使两个方法同时被执行。

WindowInsets 分发

前文我们提到,如果开发者绘制的内容出现在了系统 UI 区域内,就可能出现视觉与手势的冲突。开发者可以借助 Insets 把 view 从屏幕边缘向内移动到一个合适的位置,此时 View#onApplyWindowInsets() 会被调用。那么这些 Insets 是如何分发给 View 的呢?

笔者在 View 事件分发机制,大型职场 PUA 现场 一文中把 Android 的视图树抽象为 N 叉树

与 View 的事件分发一样,WindowInsets 的分发也是 N 叉树的遍历过程:

从 N 叉树的根节点(DecoView)开始,按照 深度优先 的方式分发给 子 view。

Android 10 和 Android 11 两个版本官方连续修改了 ViewGroup#dispatchApplyWindowInsets() 的逻辑(具体我们在源码解析篇介绍)。

如果 app targetSdkVersion < 30 ,如果某个节点消费了 Insets,所有没遍历到的节点都不会收到 WindowInsets 的分发;

当 app 运行在 Android 11 以上版本的设备上且 targetSdkVersion >= 30,如果某个节点消费了 Insets,该节点的所有子节点不会收到 WindowInsets 分发。

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

WindowInsets 分发

旧版本的分发有一个问题,无法做到两个同级的 View 同时消费 WindowInsets,如下图:

Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)

我们可以将 Level2-1 和 Level2-2 看成顶部导航和底部导航,按照旧逻辑,当 Level2-1 消费了 WindowInsets,另一个 View 便没机会了。

小结

  • 由于开发者可以将 App 内容绘制到与系统 UI 相交的位置,因此官方为开发者提供了解决视觉冲突的方式,WindowInsets
  • 开发者可以重写 View#onApplyWindowInsetsView#setOnApplyWindowInsetsListener 来根据 WindowInsets 对系统 UI 进行位置避让(对 view 设置 padding 或 margin)。
  • 下一节介绍的 fitsSystemWindows 的默认行为也是通过 onApplyWindowInsets 实现的。

部分转自互联网,侵权删除联系

赞(0) 打赏
部分文章转自网络,侵权联系删除b2bchain区块链学习技术社区 » Android Detail:Window 篇—— WindowInsets 与 fitsSystemWindow(一)求职学习资料
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

b2b链

联系我们联系我们