WindowInsets and fitsSystemWindows
Trigger
在翻看 ViewPager 源码的时候看到了这样一段代码
1 | ViewCompat.setOnApplyWindowInsetsListener(this, |
WindowInsets 是什么东西?为什么要监听?
本文尝试对 WindowInsets 作一个简单的了解。
WindowInsets
依照惯例,先从WindowInsets的官方类介绍出发
Describes a set of insets for window content.
WindowInsets are immutable and may be expanded to include more inset types in the future. To adjust insets, use one of the supplied clone methods to obtain a new WindowInsets instance with the adjusted properties.
Added in API level 20
从类介绍可以明确
- WindowInsets 是 window content 的插入物。
- WindowInsets 是不可更改的,更改的话需要 clone 生成一个更改过相应属性的 WindowInsets 实例。
还是很抽象,通过查看类的方法描述后可得知 WindowInsets 又区分为3种(more in the future):
- SystemWindowInsets,被状态栏(status bar)、导航栏(navigation bar)和输入框(IME)等部分或完全遮住的区域。
- WindowDecorInsets,被framework提供的控件所部分或完全遮住的区域,包括了 action bars 、title bars 和 toolbars ,处于 pending 状态,不用管。
- StableInsets,被系统UI元素部分活完全遮住的区域,不会跟随相应元素的显示状态改变而改变。
到这里就可以对 WindowInsets 作个简单的定义:WindowInsets是插入到应用window的区域,以SystemWindowInsets为主。
那么 WindowInsets 到底有什么用呢?
在描述 WindowInsets 的作用前,在这里需要先来说说大家经常会碰到的android:fitsSystemWindows="true"
这个API。
fitsSystemWindows
谷歌在 Android 4.4(Kitkat) 引入了半透明状态栏(translucent status bar)这个概念,而fitsSystemWindows
这个API就是根据 WindowInsets 进行相应的处理,如设置 padding。
通常,我们实现半透明状态栏需要两步
- 在style.xml里设置
android:windowTranslucentStatus
属性为true
。- 在相应的布局文件里设置
android:fitsSystemWindows
属性为true
。
其中第一步是第二步的基础。
通过查看布局的 Hierarchy View 可以得知,在没有设置半透明状态栏属性时,ContentView 的父控件会被设置一个高度等同于状态栏高度的 paddingTop,这时候 fitsSystemWindows 属性并不会生效。
WindowInsets & fitsSystemWindows
我们在 Activity 里通过setContentView()
方法构建 DecorView 实例并添加到当前Activity的 PhoneWindow 。在添加到 PhoneWindow 的过程中会调用到 DecorView 的dispatchApplyWindowInsets
方法去处理由系统提供的 WindowInsets 对象,由于 DecorView 和 FrameLayout 都没有重写dispatchApplyWindowInsets
方法,所以是直接调用了 ViewGroup 的dispatchApplyWindowInsets
方法:
1 |
|
我们来看看 View 的dispatchApplyWindowInsets
方法:
1 | public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { |
DecorView 并没有设置OnApplyWindowInsetsListener
监听,所以执行 DecorView 的onApplyWindowInsets
方法,DecorView 的onApplyWindowInsets
方法并没有完全消费 WindowInsets 对象(消费了 StableInsets 和 WindowDecorInsets ),而是进行了状态栏(status bar)和导航栏(navigation bar)的一些占位操作,具体细节就不展开了。
系统提供的 WindowInsets 里的 StableInsets 和 WindowDecorInsets 被 DecorView 消费掉了,剩下交给我们处理的是 SystemWindowInsets。
说了一大堆,似乎并 没有说到 WindowInsets 和 fitsSystemWindows 到底是怎么联系起来的。
其实他们的联系就在 View 的onApplyWindowInsets
方法,上面 DecorView 重写了该方法,在一般没有设置OnApplyWindowInsetsListener
监听的 View 会执行:
1 | public WindowInsets onApplyWindowInsets(WindowInsets insets) { |
直接调用的话是会进入到fitSystemWindowsInt
方法,我们来看看fitSystemWindowsInt
方法:
1 | private boolean fitSystemWindowsInt(Rect insets) { |
代码里的computeFitSystemWindows
方法计算 padding 以及决定是否消费 SystemWindowInsets,internalSetPadding
方法负责设置 padding。
从 if 条件可以看出,在相应的布局文件里设置android:fitsSystemWindows="true"
才会执行方法内容。
fitsSystemWindows是一个深度优先的 API,第一个消费 WindowInsets 的对象是关键。
Bonus
当父布局是 LinearLayout、 RelativeLayout 和 FrameLayout 等“非质感设计”布局时,在其子 View 重载onApplyInsets
或设置 OnApplyWindowInsetsListener 获取并消费 WindowInsets 对象时会出现 WindowInsets 对象为空的情况,原因是“非质感设计”布局并没有重写onApplyWindowInsets
方法,导致深度遍历终止在了“非质感设计”布局。
这时候只需 Override 一下onApplyWindowInsets
即可。
1 | (Build.VERSION_CODES.KITKAT_WATCH) |
Conclusion
至此,对 WindowInsets 有了简单的认识。
Trigger 里的 OnApplyWindowInsetsListener 就是 ViewPager 用于消费 WindowInsets 的一个监听。
Thanks for reading. Have a nice day :-)