fitsSystemWindowsの話をつらつらと

fitsSystemWindowsについてマスターしつつあるので、つらつらと学んだことをまとめておきます。

SystemUiVisibilityの詳細な設定については説明を割愛するのでご了承ください。

そもそもfitsSystemWindowsとは?

Android端末には、status bar、navigation barなどのSystem UIと総称されるViewがあります。 デフォルトでは、System UIにコンテンツの要素が被ることはありません。そこには制約があります。 しかし、SystemUiVisibilityの設定を変えることで、コンテンツの要素をSystem UIの裏側描くことが可能になります。



右の図がSystemUiVisibilityの設定を変更したものです。画像がstatus bar、navigation barの背後に描画されていることが分かります。

ここからが本題です。上記の画像の場合は、画像をめいいっぱいに広げて表示しても違和感がありません。しかし、AppBarLayoutといったToolbarの場合はどうなるでしょうか?



右の図のAppBarLayoutが、status barに食い込んでしまっていることが分かります。SystemUiVisibilityの設定を変えている場合、status barの高さを考慮する必要があることが分かります。

このときに、fitsSystemWindowsを使うとSystem UIに被らないようにコンテンツを配置することが出来ます。

<LinearLayout android:fitsSystemWindows="true">
  <com.google.android.material.appbar.AppBarLayout />
  ...
</LinearLayout>


見た目が元に戻りました。fitsSystemWindowsをつけると、どのViewの要素が変化するかをLayout Inspectorで確認します。



paddingTopと、paddingBottomに値が指定されていることが分かります。これはfitsSystemWindowsのデフォルトの振る舞いによるものです。

fitsSystemWindowsはデフォルトで、paddingTopにstatus barの高さを、paddingBottomにnavigation barの高さを設定します。それにより、コンテンツがSystem UIに被らないようになります。

まとめると「fitsSystemWindowsはSystem UIの高さに応じてpaddingに値をセットする」振る舞いをします。

しかし、これはデフォルトの動作で、fitsSystemWindowsの動作を変更することが出来ます。

AppBarLayoutはfitsSystemWindowsのデフォルトの動作を変更しているViewなので、AppBarLayoutにfitsSystemWindowsをつけた場合にどのように動作するかを見てみます。

AppBarLayoutのfitsSystemWindowsの解釈

fitsSystemWindowsをAppBarLayoutに設定します。

<LinearLayout>
  <com.google.android.material.appbar.AppBarLayout
    android:fitsSystemWindows="true" />
  ...
</LinearLayout>


見た目はおかしくないのですが、paddingが変更されていません。じゃあどこが変わったかというと、heightの値が変わっています。



省略するのですが、fitsSystemWindowsを指定しない場合のAppBarLayoutの高さは154でした。上記の図の220という数字は 154(元の高さ) + 66(status barの高さ) = 220です。

AppBarLayoutはpaddingTopを設定するのではなく、高さを調整することでSystem UI上の見た目を調整しています。

このように、Viewによってはカスタムの振る舞いを提供しています。他には有名どころだと、CoordinatorLayout、DrawerLayoutも特別な振る舞いを提供しています。

最後にどのようにして、カスタムの振る舞いを実装するかを説明します。

fitsSystemWindowsのカスタマイズ

View.setOnApplyWindowInsetsListenerから、OnApplyWindowInsetsListenerを設定しておくと、デフォルトのfitsSystemWindowsの振る舞いではなく、設定したOnApplyWindowInsetsListenerのほうがコールバックされます。

OnApplyWindowInsetsListenerには、WindowInsetsが渡ってきます。この中にはSystem UIのサイズと、WindowInsetsが消費されたかどうかを表すフラグがあります。

例えば、status barの2倍のサイズのpaddingTopを設定して、これ以降のViewにSystem WindowInsetsを渡したくないときは次のようにします。

binding.appbar.setOnApplyWindowInsetsListener { v, insets ->
  binding.appbar.updatePadding(top = insets.systemWindowInsetTop * 2)
  insets.consumeSystemWindowInsets()
}

このAPIを使うことで、fitsSystemWindowsよりもはるかに細かくSystem UIを制御することが出来ます。

CoordinatorLayoutなどのViewもこのAPIを使って細かくWindowInsetsを制御をしています。

個人的なハマりどころ

fitsSystemWindowsは複数つけても意味がない

これはデフォルトの場合の挙動なのですが、fitsSystemWindowsはWindowInsetsを消費するので、一度でも使ってしまうとそれ以降のViewに対して影響を与えません。

<LinearLayou android:fitsSystemWindows="true">
  <ImageView android:fitsSystemWindows="true" />
  ...
</LinearLayout>

なので、この場合ImageViewにつけたfitsSystemWindowsは意味がないです。親のLinearLayoutで既に消費されているためです。

しかし、AppBarLayoutなどの特殊な振る舞いをするViewの場合は、子供のfitsSystemWindowsに意味があったりするので注意が必要です。

また、ViewによってSystem WindowInsetsを消費したり、しなかったりするのでそこも注意が必要です。DrawerLayoutは消費し、AppBarLayoutは消費しません。 既に、消費済みの場合、OnApplyWindowInsetsListenerを登録してもコールされないので注意してください。

まとめ

Written by
あんどろいどでぃべろっぱぁー🍎