日々是好日

プログラミングについてのあれこれ、ムダ知識など

CoordinatorLayout+Toolbar+BottomNavigation+FAB使用時のメモ

RecyclerViewのスクロールにあわせてToolbarやBottomNavigationView, FloatingActionButtonを隠そうとしたら、いろいろとハマったのでメモ。

何をしたいのか

こんな感じにRecyclerViewのスクロールにあわせて各要素を隠したい。

f:id:kcpoipoi:20191110234927g:plain

動かす要素は次のとおり。

  • Toolbar
  • AdView
  • BottomNavigationView
  • FloatingActionButton

レイアウト

Data Bindingを利用するためルートは<layout>になっていますが、各要素がCoordinatorLayoutの直接の子になっていればOK。

<layout ...>
  <data>
    ...
  </data>

  <androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
      android:id="@+id/appBarLayout"
      ...>

      <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbar"
        ...
        app:layout_scrollFlags="scroll|enterAlways" />

    </com.google.android.material.appbar.AppBarLayout>

    <FrameLayout
      android:id="@+id/mainFrag"
      ...
      app:layout_behavior="@string/appbar_scrolling_view_behavior"
      />

    <com.google.android.gms.ads.AdView
      android:id="@+id/adView"
      ...
      app:layout_behavior=".list.behavior.BottomWidgetBehavior"
      />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
      android:id="@+id/floatingActionButton"
      ...
      app:layout_behavior=".list.behavior.FabBehavior"
      />

    <com.google.android.material.bottomnavigation.BottomNavigationView
      android:id="@+id/bottomNavigation"
      ...
      app:layout_behavior=".list.behavior.BottomWidgetBehavior"
      />

  </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

構造だけ抜き出すとこんな感じ。

<layout>
  <data></data>

  <androidx.coordinatorlayout.widget.CoordinatorLayout>
    
    <com.google.android.material.appbar.AppBarLayout>

      <androidx.appcompat.widget.Toolbar/>

    </com.google.android.material.appbar.AppBarLayout>

    <FrameLayout/>

    <com.google.android.gms.ads.AdView/>

    <com.google.android.material.floatingactionbutton.FloatingActionButton/>

    <com.google.android.material.bottomnavigation.BottomNavigationView/>

  </androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

真ん中あたりのFrameLayoutRecyclerViewを含むFragmentをInflateし、そのスクロールをトリガーとして他のViewを動作させます。

CoordinatorLayout

Viewのスクロールにあわせて他のViewを動かす場合、前提としてCoordinatorLayoutの直下にないとだめ。 また、スクロールするViewはNestedScrollViewRecyclerViewでないとだめ。

developer.android.com

Toolbarの場合

AppBarLayoutを親要素とし、その中に配置する。 スクロールにあわせてどんな動作をさせるかは、app:layout_scrollFlagsにて定義する。

パラメータについては次の記事が参考になりそう。

medium.com

AdView, BottomNavigationViewの場合

自前でカスタムBehaviorを定義し、app:layout_behaviorに設定する必要がある。

CoordinatorLayout.Behaviorを継承し、onStartNestedScroll及びonNestedPreScrollをオーバーライドする。

class BottomWidgetBehavior<V : View>(context: Context, attrs: AttributeSet) :
  CoordinatorLayout.Behavior<V>(context, attrs) {

  override fun onStartNestedScroll(...): Boolean {
    return axes == ViewCompat.SCROLL_AXIS_VERTICAL
  }

  override fun onNestedPreScroll(...) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
    child.translationY = max(0f, min(child.height.toFloat() + child.marginBottom, child.translationY + dy))
  }
}

child.translationYにて、ViewのY座標を逐次書き直すことで隠す動作を表現する。 child.marginBottomを加算することで、marginBottomを設定しているAdViewにも対応可能。

FABの場合

FABはFloatingActionButton.Behaviorを継承し、同じくonStartNestedScroll及びonNestedPreScrollをオーバーライドする。

なお、FABはスクロール時にshowhideで表示非表示を切り替えてるが、hideはリスナーをセットしないとshowが呼ばれないので要注意。

class FabBehavior(context: Context, attrs: AttributeSet) :
  FloatingActionButton.Behavior(context, attrs) {

  override fun onStartNestedScroll(...): Boolean {
    return axes == ViewCompat.SCROLL_AXIS_VERTICAL
  }

  override fun onNestedPreScroll(...) {
    super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
    if (dy > 0 && child.visibility == View.VISIBLE) {
      // User scrolled down and the FAB is currently visible -> hide the FAB
      child.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
        override fun onHidden(fab: FloatingActionButton?) {
          super.onHidden(fab)
          fab?.visibility = View.INVISIBLE
        }
      })
    } else if (dy < 0 && child.visibility != View.VISIBLE) {
      // User scrolled up and the FAB is currently not visible -> show the FAB
      child.show()
    }
  }
}

それぞれ、作成したBehaviorをapp:layout_behaviorに設定すればおk。

リポジトリ

github.com