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
をスクロール量に応じて動的に設定しないと、スクロールの挙動がおかしくなるのですが、
ちょっと紙面が足りないのでここまで。。。