DecorViewにOnApplyWindowInsetsListenerをセットするとstatusBarBackgroundとnavigationBarBackgroundがなくなる件

# 要約

ViewにOnApplyWindowInsetsListenerを登録したときはそのViewのonApplyWindowInsetsをコールする必要が無いのかちゃんと考えましょう。

```kotlin
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, insets ->
    // ごにょごにょ
    WindowInsetsCompat.toWindowInsetsCompat(view.onApplyWindowInsets(insets.toWindowInsets()))
}
```

# 背景

ステータスバーやナビゲーションバーなどの領域情報を受け取るためにはsetOnApplyWindowInsetsListenerを使ってWindowInsetsの値を受け取ります。

WindowInsetsは親Viewから子Viewへと伝搬していきますが、途中でInsetsの情報が消費されることもあります。
親がすでにオフセットを持っているなら子Viewがさらにオフセットを取るとおかしくなるので、当たり前の挙動ではあります。
（OnApplyWindowInsetsListener#onApplyWindowInsetsが戻り値を持つのは値の消費を伝えるためですね）

しかし、何らかの理由で消費される前の情報を拾いたいという場合は、decorViewにリスナーを設定すればいいじゃんとなるかもしれません。

※DecorViewにOnApplyWindowInsetsListenerをsetすることがそもそも良いのかそういうのは置いておきます

```kotlin
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, insets ->
    // insets使ってごにょごにょ
    insets
}
```

しかしそうすると、Listenerを設定しただけだというのに、ステータスバーとナビゲーションバーの表示がおかしくなります。

|なにもしない|OnApplyWindowInsetsListenerをセット|
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/f362f856-2a55-4626-e007-144dd87f23be.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/a4722091-e4c5-a542-989e-fc461537ba77.png)|

どうなっているのか、layout insepectorで見てみましょう。

|||
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/c33129f2-c3cb-1668-bfd3-ce38455a30e3.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/d60fb3d6-22c7-2cfb-7d79-6bc9ef747fcc.png)|

navigationBarBackgroundとstatusBarBackgroundが無くなっていますね。

setOnApplyWindowInsetsListenerでリスナーを登録するとどうなるのかを調べてみましょう。

```java:View.java
public void setOnApplyWindowInsetsListener(OnApplyWindowInsetsListener listener) {
    getListenerInfo().mOnApplyWindowInsetsListener = listener;
}
```

リスナーはListenerInfoに登録されます。
このリスナーはdispatchApplyWindowInsetsでコールされます。

```java:View.java
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
    try {
        mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
        if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
            return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
        } else {
            return onApplyWindowInsets(insets);
        }
    } finally {
        mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
    }
}
```

見ての通り、リスナーが登録されているとOnApplyWindowInsetsListenerのonApplyWindowInsetsがコールされ、登録されていなければ、そのViewのonApplyWindowInsetsがコールされます。

つまり、そのViewがonApplyWindowInsetsにある本来コールされるべき処理がリスナーを登録したことで実行されなくなります。
そして、DecorViewはonApplyWindowInsetsにがっつりと本来実行されるべき処理があり、ここでnavigationBarBackgroundとstatusBarBackgroundといったViewを作り追加する処理もあります。

```java:DecorView.java
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    final WindowManager.LayoutParams attrs = mWindow.getAttributes();
    mFloatingInsets.setEmpty();
    if ((attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0) {
        // For dialog windows we want to make sure they don't go over the status bar or nav bar.
        // We consume the system insets and we will reuse them later during the measure phase.
        // We allow the app to ignore this and handle insets itself by using
        // FLAG_LAYOUT_IN_SCREEN.
        if (attrs.height == WindowManager.LayoutParams.WRAP_CONTENT) {
            mFloatingInsets.top = insets.getSystemWindowInsetTop();
            mFloatingInsets.bottom = insets.getSystemWindowInsetBottom();
            insets = insets.inset(0, insets.getSystemWindowInsetTop(),
                    0, insets.getSystemWindowInsetBottom());
        }
        if (mWindow.getAttributes().width == WindowManager.LayoutParams.WRAP_CONTENT) {
            mFloatingInsets.left = insets.getSystemWindowInsetTop();
            mFloatingInsets.right = insets.getSystemWindowInsetBottom();
            insets = insets.inset(insets.getSystemWindowInsetLeft(), 0,
                    insets.getSystemWindowInsetRight(), 0);
        }
    }
    mFrameOffsets.set(insets.getSystemWindowInsetsAsRect());
    insets = updateColorViews(insets, true /* animate */);
    insets = updateStatusGuard(insets);
    if (getForeground() != null) {
        drawableChanged();
    }
    return insets;
}
```

DecorViewにOnApplyWindowInsetsListenerを登録すると、本来実行されるはずだった、これらの処理が実行されなくなるからおかしくなっていたわけですね。これはDecorViewだけでなく、`android:fitsSystemWindows`の処理など、onApplyWindowInsetsで実行されるはずの何らかの処理がある場合も同様です。

# 解決方法

OnApplyWindowInsetsListenerで情報を拾いつつ、本来実行されるはずの処理を実行するには、OnApplyWindowInsetsListenerの中でonApplyWindowInsetsをコールします。

```kotlin
ViewCompat.setOnApplyWindowInsetsListener(window.decorView) { view, insets ->
    // ごにょごにょ
    WindowInsetsCompat.toWindowInsetsCompat(view.onApplyWindowInsets(insets.toWindowInsets()))
}
```

ViewCompatを使っている場合WindowInsetsCompatをWindowInsetsに変換して渡し、戻り値のWindowInsetsをWindowInsetsCompatに変換しないといけないので記述が長くなります。

こうすることで、本来の処理を邪魔しないように情報だけ拾うことができるようになります。

|なにもしない|OnApplyWindowInsetsListenerをセット|
|:-:|:-:|
|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/f362f856-2a55-4626-e007-144dd87f23be.png)|![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155171/41d467cc-4e0e-f867-b3e0-16027db5bb54.png)|

リスナーを登録するとそのViewが持っている処理より、リスナーコールが優先されるという動作は、`OnTouchListener`なども同じ挙動ではあるのですが、中の動作を理解していないとかなり戸惑うことになると思います。というか、私が戸惑いました。

以上です。