2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

要約

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

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することがそもそも良いのかそういうのは置いておきます

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

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

なにもしない OnApplyWindowInsetsListenerをセット

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

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

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

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

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

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を作り追加する処理もあります。

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をコールします。

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

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

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

なにもしない OnApplyWindowInsetsListenerをセット

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

以上です。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?