CoordinatorLayout+BottomNavigationView+NestedScrollView+その他諸々で画面構築してみた
次のぎじゅつを使って画面構築してみたのでメモ
- 見た目はToolbar+コンテンツ表示Fragment+BottomNavigationViewの画面構成
- コンテンツ表示Fragmentは、BottomNavigationViewの選択により入れ替える
- Toolbarはコンテンツ表示Fragmentのスクロール動作により隠れるようにする
- Toolbarのタイトルを、Navigationを使ってBottomNavigationViewに連動させる
- BottomNavigationViewは固定する
BottomNavigationView & Toolbar & Navigation 間の連携に微妙なコツがあったり、 そもFrameLayoutであることを考慮したり、いろいろハマりまくった。
スクロールでBottomNavigationViewも隠したい場合は、カスタムBehavior定義して設定してやればおけ。 個人的には画面切り替えにスクロール動作が増えてしまうので、あえて固定する方法をとった。
動作プレビュー
動きはこんな感じです。 BottomNavigationViewのMenuをタップしてコンテンツ切り替え。

レイアウトXML
main_activity.xmlはこんな感じ。属性は適宜省略しています。
<layout> <data> <variable name="viewModel" type="MainActivityViewModel" /> </data> <androidx.coordinatorlayout.widget.CoordinatorLayout> <com.google.android.material.appbar.AppBarLayout> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:minHeight="?attr/actionBarSize" app:layout_scrollFlags="scroll|enterAlways" /> </com.google.android.material.appbar.AppBarLayout> <!-- コンテンツ表示Fragment --> <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_container" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="start" android:layout_marginBottom="?attr/actionBarSize" app:defaultNavHost="true" app:layout_behavior="@string/appbar_scrolling_view_behavior" app:navGraph="@navigation/nav_graph" /> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_navigation" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" app:labelVisibilityMode="unlabeled" app:menu="@menu/bottom_menu" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> </layout>
コンテンツの一例はコレ。
<layout>の直下に<NestedScrollView>を入れておくと、Fragmentがinflateされたときにイイ感じにFragmentContainerViewに展開してくれる
(<merge>タグと同じ働きをしてくれるようです。)。
<layout> <data> <variable name="viewModel" type="MainActivityViewModel" /> </data> <androidx.core.widget.NestedScrollView> <LinearLayout> <androidx.recyclerview.widget.RecyclerView/> <!-- リソースもバインドできる --> <include android:id="@+id/tag" layout="@layout/chip_group_layout" app:groupName="@{@string/tag}" /> <include android:id="@+id/bland" layout="@layout/chip_group_layout" app:groupName="@{@string/bland}" /> </LinearLayout> </androidx.core.widget.NestedScrollView> </layout>
実装編
NavigationとToolbar/BottomNavigationViewの連携の実装
コンテンツのレイアウトを作成する
home_fragment.xml等、必要なコンテンツのレイアウトを作る。
気を付けるところは、スクロールイベントをCoordinatorLayoutで拾うため、
必ず<layout>直下に<RecyclerView>または<NestedScrollView>を配置すること。
前節を参照してください。
BottomNavigationViewのMenuを作成する
BottomNavigationViewに表示するメニューを作る(res/menu/bottom_navigation.xml)。
ここで設定したidを使って、次のNavigation graphとの紐付けを行います。
<menu> <item android:id="@+id/home" android:icon="@drawable/ic_home_24px" android:title="@string/home" /> <item android:id="@+id/create" android:icon="@drawable/ic_add_box_24px" android:title="@string/create" /> ... </menu>
Navigation graphを作成する
res/navigation/nav_graph.xmlを作成する。
今回、特定のFragmentからの遷移は実装しないので、同じような<fragment>要素をコンテンツの数だけ作っていきます。
ここでポイント。前節のMenuのidを<fragment>の各idと一致させることで、Menuのクリックイベントの分岐処理が不要になります。
また、Toolbarに表示されるタイトルには、自動的にlabel属性の値が適用されます。
<navigation android:id="@+id/nav_container" app:startDestination="@id/home"> <fragment android:id="@+id/home" android:name="package.HomeFragment" android:label="Home" tools:layout="@layout/home_fragment" /> <fragment android:id="@+id/create" android:name="package.CreateArticleFragment" android:label="Create Article" tools:layout="@layout/create_article_fragment" /> ... </navigation>
FragmentContainerViewにnav_graphをセット
次にMainActivityを実装する。
Toolbar及びBottomNavigationViewとNavigationとの紐付けは、setupWithNavControllerにより行います。
ここで2点ほどポイント。
- onCreate内でfindNavControllerを直接呼ぶと落ちる
IllegalStateExceptionの発生
- setSupportActionBar(it.toolbar)は最後の方で呼ぶ
1はsupportFragmentManagerを経由することで回避できます。
onCreateの中で出来る限りViewへの操作は完結させたいので、下記のように記述しました。
class MainActivity : AppCompatActivity() {
private val viewModel: MainActivityViewModel by viewModels {
MainActivityViewModelFactory(application)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DataBindingUtil.setContentView<MainActivityBinding>(this, R.layout.main_activity).let {
it.viewModel = viewModel
it.lifecycleOwner = this
// ここでfindNavController(R.id.nav_container)を直接呼ぶと落ちる
// リファレンスに記載あり
supportFragmentManager.findFragmentById(R.id.nav_container)?.findNavController()?.let { nc ->
it.toolbar.setupWithNavController(nc)
it.bottomNavigation.setupWithNavController(nc)
}
setSupportActionBar(it.toolbar)
}
}
}
これでBottomNavigationViewの選択によりコンテンツが切り替わり、さらにToolbarのタイトルも変化するようになります。
スクロール時にToolbarを隠す動作の実装
わりとおなじみな実装にはなりますが、一応書いておきます。
基本となる画面構造
テンプレにしてもいいんじゃないかと思う基本構造。
CoordinatorLayoutはFrameLayoutベースだから、BottomNavigationView分の高さを削るためにはmarginを設定すればいい.
BottomNavigationViewも隠すなら、layout_marginBottomを消してBottomNavigationViewにBehaviorを追加する。
これだけで、あとはCoordinatorLayoutがよしなにやってくれるので助かる。
<CoordinatorLayout> <AppBarLayout> <Toolbar android:minHeight="?attr/actionBarSize" app:layout_scrollFlags="scroll|enterAlways"/> </AppBarLayout> <FragmentContainerView app:layout_behavior="@string/appbar_scrolling_view_behavior" android:layout_marginBottom="?attr/actionBarSize"/> <BottomNavigationView /> </CoordinatorLayout>
NestedScrollView内にRecyclerViewを配置する場合の注意
少し逸れますが、いろいろ調べてる中で地雷があったのでメモ。
NestedScrollViewは子要素をラップするようにレイアウトのサイズが変わるので、 RecyclerViewをネストすると、アイテムが全て展開されてViewがリサイクルされなくなるとのこと。
そんなわけで、コンテンツFragment内で子Viewのサイズを固定値に再設定する必要があります。
override fun onCreateView(...) {
binding.also {
...
it.recyclerView.setOnGlobalLayout {
// WindowManagerからアプリの画面高さを取得し、ActionBarの高さを差し引く
val rvHeight = screenHeight - actionbarHeight
it.recyclerView.layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, rvHeight)
}
}
...
}
rvHeightはアプリの画面高さからActionBarの高さ(=BottomNavigationViewの高さ)を差し引いたもので、
RecyclerViewの高さを固定値に変更してやります。
また、nestedScrollFlagをスクロール量に応じて動的に設定しないと、スクロールの挙動がおかしくなるのですが、
ちょっと紙面が足りないのでここまで。。。