BottomNavigationViewの上にSnackbarが表示されるようにしつつFABも連動して動くようにする

BottomNavigationViewを使ってみようかなと思ったときに、ふと「Snackbarはどこに表示されるのが正しいのか」ということを疑問に思った。ガイドラインではBottomNavigationViewの上からSnackbarが現れるようにするということが書いてあった。

https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-specs

Elevation的にはSnackbarがBottomNavigationViewより下にあるので、「下に配置する」というべきなんで、上から現れると表現するのも誤解がありそうな気がして気持ち悪い。

実装

挙動はわかったが、ではそれをどうやって実装すればよいのかという話になると、これがややこしい。いろいろ探し回ったが、こちらのサイトを参考にするのが良さそうな感じであった。

https://sakebook.hatenablog.com/entry/2017/02/12/032501

結論から言うとCoordinatorLayout.Behaviorを継承して、カスタムビヘイビアを使って実装するしかないようだ。今のところは。それとも、もしかしたら、私が見つけられなかっただけで、もっと簡単な方法があるのかもしれない。

FABを追加した場合はどうするのか

BottomNavigationViewを配置して、さらにFABも一緒に配置したい場合はどうするのか。

つまりこういう動きをしたい、ということである。

デモ
  • SnackbarはBNVの上辺から現れる
  • FABはSnackbarを避ける
  • SnackbarはBNVの動きに合わせて動く=FABも連動して動く
  • FABはBNVも避ける
  • BNVはスクロールに合わせて隠れる(Appbarが隠れるのと連動する)

単純にSnackbarがBNVの上辺から出現してくれれば(SnackbarがBNVを避けてくれれば)ことは簡単なのだが、そういう設定にたどり着くことができず、最終的にcustom behaviorでゴリ押しした。

コードはGitHubにあげておいた。

どうやったか

FABがBNVを避ける

これは原理をいまだ理解していないのだが、BNVにapp:insetEdge="bottom"を加えることでFABがBNVを避けるようになる。

これに気づくまでが非常に長くて、ここで俺の苦労を聞いてくれと言いたいところだが割愛する。とりあえず、FABがSnackbarを避けるのはBehaviorによるものではなかったというのが今回の作業で得られたもっとも大きな収穫かもしれない。

insetEdgeの挙動に詳しい人、もしくは詳しく解説したブログ記事なんかをご存じの方は教えて欲しい。

BNVを隠す

スクロールに合わせてBNVを隠す。

このあたりからこちらのサイトを参考にしだす。

https://sakebook.hatenablog.com/entry/2017/02/12/032501

私はAppbarLayoutが隠れている比率を計算して、同じ比率だけBNVを隠すという実装を行った。最初はAppbarLayoutのBehaviorを真似しようと思ったが、ややこしかったので途中で諦めた。

ちなみにAppbarLayoutを動かさないで、この仕様を取り入れたいという場合は、onNestedScrollなどを使って自分で隠すようにする必要があるだろう。

この実装にしたのはその手動計算が面倒くさかったというのもある。

やり方としては

  1. custom behaviorで`layoutDependsOn`を使いAppbarLayoutに依存するように宣言
  2. `onDependentViewChanged`でAppbarLayoutがどれだけ隠れているかを計算する
  3. 同メソッド内でBNVの`setTranslationY`を使ってBNVを隠す
やっていることはこれだけである。

SnackbarをBNVの上に表示する

これが一番苦労した。参考にしたサイトでは、Snackbar表示中はBNVを動かさない、というやり方での実装だった。私の場合はSnackbar表示中だろうとBNVは動くし、それに合わせてSnackbarも動く。

  1. custom behaviorで`oayoutDependsOn`を使いSnackbar.SnackbarLayoutに依存するよう宣言
  2. `onDependentViewChanged`でSnackbarが出現したことをフラグで持つ
  3. Snackbar表示中は、`onNestedPreScroll`でSnackbarのpaddingを更新する
  4. `onDependentViewRemoved`でSnackbarが消えたらフラグをクリアする
なぜ`onDependentViewChanged`のみでやらないのかというと、このメソッドはSnackbarがニョキッと動いている最中は呼ばれるのだが、完全に表示されてSnackbarが停止した状態では呼び出されない。そのため、Snackbarが停止している間にBNVを動かすと、その間はSnackbarが置いてけぼりになってしまうからだ。

BNVの動きに連動してSnackbarのpaddingを更新しなければならないので、こんな変な実装になってしまった。

BNVのbehaviorがSnackbarの動きを制御するという若干の気持ち悪さがあるが、他に方法を思いつかなかった。

insetEdgeをうまく使えばもっと簡単なのでは?

と思っていろいろ試したのだけど、結局良くわからなかったのでこのような実装になった。

insetEdgeのinsetが何のことかよくわかっていない。似たようなやつにdodgeInsetEdgeなるものもある。dodgeInsetEdge="bottom"を設定したら、画面上部に向かってViewが飛んでいって、呪いの館を思い出した。

insetEdgeの使い方を詳しく解説しているサイトをご存知だったら教えて欲しい。

コードの全体(といっても、重要なのはcustom behaviorだけ)はGitHubにあるので参照してほしい。

ちなみにこのコードはsupport library 25.3.1で動作確認している。バージョンによって挙動が変わると思うので、注意してほしい。

Amazonのほしいものリストを公開しています。仕事で欲しいもの、単なる趣味としてほしいもの、リフレッシュのために欲しいものなどを登録しています。 寄贈いただけると泣いて喜びます。大したお礼はできませんが、よりよい情報発信へのモチベーションに繋がりますので、ご検討いただければ幸いです。