Trigger

在翻看 ViewPager 源码的时候看到了这样一段代码

1
2
3
4
5
6
7
8
ViewCompat.setOnApplyWindowInsetsListener(this,
new android.support.v4.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(final View v,
final WindowInsetsCompat originalInsets) {
// ***
}
});

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

从类介绍可以明确

  1. WindowInsets 是 window content 的插入物。
  2. WindowInsets 是不可更改的,更改的话需要 clone 生成一个更改过相应属性的 WindowInsets 实例。

还是很抽象,通过查看类的方法描述后可得知 WindowInsets 又区分为3种(more in the future):

  1. SystemWindowInsets,被状态栏(status bar)、导航栏(navigation bar)和输入框(IME)等部分或完全遮住的区域。
  2. WindowDecorInsets,被framework提供的控件所部分或完全遮住的区域,包括了 action bars 、title bars 和 toolbars ,处于 pending 状态,不用管。
  3. StableInsets,被系统UI元素部分活完全遮住的区域,不会跟随相应元素的显示状态改变而改变。

到这里就可以对 WindowInsets 作个简单的定义:WindowInsets是插入到应用window的区域,以SystemWindowInsets为主。

那么 WindowInsets 到底有什么用呢?

在描述 WindowInsets 的作用前,在这里需要先来说说大家经常会碰到的android:fitsSystemWindows="true"这个API。

fitsSystemWindows

谷歌在 Android 4.4(Kitkat) 引入了半透明状态栏(translucent status bar)这个概念,而fitsSystemWindows这个API就是根据 WindowInsets 进行相应的处理,如设置 padding。

通常,我们实现半透明状态栏需要两步

  1. 在style.xml里设置 android:windowTranslucentStatus属性为true
  2. 在相应的布局文件里设置 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
// 调用View的dispatchApplyWindowInsets方法处理insets
insets = super.dispatchApplyWindowInsets(insets);
// 判断insets是否被消费
if (!insets.isConsumed()) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
// insets没有被消费的的话就依照顺序分发给子view
insets = getChildAt(i).dispatchApplyWindowInsets(insets);
// 有子view消费insets,退出循环
if (insets.isConsumed()) {
break;
}
}
}
return insets;
}

我们来看看 View 的dispatchApplyWindowInsets方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
try {
// 把PFLAG3_APPLYING_INSETS标记加入到mPrivateFlag3
mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
// 倘若View有注册OnApplyWindowInsetsListener监听就直接调用onApplyWindowInsets,
// 否则执行onApplyWindowInsets方法
if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
} else {
return onApplyWindowInsets(insets);
}
} finally {
// 将PFLAG3_APPLYING_INSETS标记从mPrivateFlag3移除
mPrivateFlags3 &= ~PFLAG3_APPLYING_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
2
3
4
5
6
7
8
9
10
11
12
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
if (fitSystemWindows(insets.getSystemWindowInsets())) {
return insets.consumeSystemWindowInsets();
}
} else {
if (fitSystemWindowsInt(insets.getSystemWindowInsets())) {
return insets.consumeSystemWindowInsets();
}
}
return insets;
}

直接调用的话是会进入到fitSystemWindowsInt方法,我们来看看fitSystemWindowsInt方法:

1
2
3
4
5
6
7
8
9
10
11
12
private boolean fitSystemWindowsInt(Rect insets) {
if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
...
boolean res = computeFitSystemWindows(insets, localInsets);
mUserPaddingLeftInitial = localInsets.left;
mUserPaddingRightInitial = localInsets.right;
internalSetPadding(localInsets.left, localInsets.top,
localInsets.right, localInsets.bottom);
return res;
}
return false;
}

代码里的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
2
3
4
5
6
7
8
9
10
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH)
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
int childCount = getChildCount();
for (int index = 0; index < childCount; ++index)
getChildAt(index).dispatchApplyWindowInsets(insets);
// let children know about WindowInsets

return insets;
}

Conclusion

至此,对 WindowInsets 有了简单的认识。

Trigger 里的 OnApplyWindowInsetsListener 就是 ViewPager 用于消费 WindowInsets 的一个监听。

Thanks for reading. Have a nice day :-)

Reference