7、NavigationUI与菜单:集成Toolbar/ActionBar、使用NavigationUI自动处理菜单项点击、配置DrawerLayout
说实话,Navigation 组件最让我觉得「真香」的地方,就是它和 UI 层的深度绑定。你想想看,以前我们要手动管理 Fragment 切换时的标题变化、菜单项的显示隐藏、DrawerLayout 的联动……代码写起来又臭又长。NavigationUI 这个工具类,就是专门来解决这些痛点的。
我个人习惯把 NavigationUI 理解为「导航和 UI 之间的桥梁」。它不需要你写一堆监听器,也不需要你在每个 Fragment 里重复设置标题。只要配置得当,它自己就能搞定一切。
7.1 集成 Toolbar 与 ActionBar
先说说最基础的需求——标题栏。单 Activity 架构下,我们通常用 Toolbar 替代传统的 ActionBar。NavigationUI 可以自动把当前目的地的 label 显示到标题上。
具体怎么做?我直接上代码:
// 在 Activity 的 onCreate 中
val navController = findNavController(R.id.nav_host_fragment)
val toolbar = findViewById<Toolbar>(R.id.toolbar)
// 关键就这一行
NavigationUI.setupActionBarWithNavController(this, navController)
嗯,这里要注意:如果你的 Activity 使用了 setSupportActionBar(toolbar),那么上面这行代码会自动把导航控制器的当前目的地名称设置到标题上。比如你的 nav_graph 里有一个 homeFragment,它的 label 是「首页」,那标题栏就会显示「首页」。
android:label=""。我在项目中经常用这个特性来处理一些全屏页面。
但有个坑——返回按钮。默认情况下,NavigationUI 会根据返回栈自动显示或隐藏 Toolbar 上的返回箭头。但前提是你得在 Activity 里重写 onSupportNavigateUp():
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
我曾经遇到过一个 bug:返回按钮点了没反应。排查了半天,发现是忘了重写这个方法。你想想看,NavigationUI 帮你处理了 UI 层,但底层的导航逻辑还是得你自己桥接一下。
7.2 使用 NavigationUI 自动处理菜单项点击
接下来是菜单。以前我们处理菜单项点击,通常是在 onOptionsItemSelected() 里写一堆 when 分支。NavigationUI 提供了一种更优雅的方式——直接把菜单项和目的地绑定。
做法很简单:
- 在 menu 资源文件中定义菜单项,
android:id要和 nav_graph 中目的地的id保持一致。 - 在 Activity 或 Fragment 中调用
NavigationUI.onNavDestinationSelected()。
看个例子:
<!-- menu/main_menu.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/settingsFragment"
android:title="设置" />
<item
android:id="@+id/profileFragment"
android:title="个人中心" />
</menu>
// Activity 中
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(navController)
|| super.onOptionsItemSelected(item)
}
这样,当用户点击「设置」菜单项时,NavigationUI 会自动导航到 settingsFragment。不需要你写任何导航逻辑。
onNavDestinationSelected() 会返回 false,然后走默认的 super 逻辑。所以不用担心崩溃问题。
我个人觉得这个设计很巧妙。它把「菜单项点击」和「导航」这两个概念解耦了。你只需要关心菜单项长什么样,导航的事交给 NavigationUI 去操心。
7.3 配置 DrawerLayout
DrawerLayout 是 NavigationUI 的另一个重头戏。说实话,以前写 DrawerLayout 的代码,光处理菜单项的选中状态、标题同步、打开关闭监听,就能写上百行。NavigationUI 把这些全包了。
配置步骤也不复杂:
- 在布局文件中使用 DrawerLayout 包裹 NavigationView 和内容区域。
- 在 Activity 中调用
NavigationUI.setupWithNavController()。
核心代码就这几行:
val navController = findNavController(R.id.nav_host_fragment)
val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)
val navigationView = findViewById<NavigationView>(R.id.nav_view)
// 绑定 NavigationView 和 NavController
NavigationUI.setupWithNavController(navigationView, navController)
// 可选:让 Toolbar 的汉堡图标控制 Drawer 的打开/关闭
NavigationUI.setupActionBarWithNavController(this, navController, drawerLayout)
这里有个细节:setupWithNavController() 会自动做三件事:
- 监听 NavigationView 的菜单项点击,自动导航到对应目的地。
- 当导航发生时,自动更新 NavigationView 的选中状态。
- 根据当前目的地,自动打开或关闭 DrawerLayout(如果你配置了
drawerLayout参数)。
我曾经在项目里遇到过一个奇怪的问题:DrawerLayout 的菜单项点击后,页面跳转了,但菜单项的选中状态没有更新。后来发现是因为我在 nav_graph 里用了 popUpTo 和 popUpToInclusive,导致返回栈的层级变了。NavigationUI 的选中状态是基于返回栈的「栈顶目的地」来判断的。如果你手动清除了返回栈,它可能就找不到正确的选中项了。
popUpTo,建议在 setupWithNavController() 之后,手动调用 navigationView.setCheckedItem(R.id.your_destination_id) 来确保选中状态正确。
另外,DrawerLayout 的标题同步也是自动的。当你导航到某个目的地时,Toolbar 的标题会自动更新为该目的地的 label。如果你不想让某个页面显示 DrawerLayout 的汉堡图标(比如登录页面),可以在 nav_graph 中给该目的地添加 app:startDestination="true" 属性,或者手动控制 drawerLayout.setDrawerLockMode()。
7.4 知识体系总览
说了这么多,我画了一张图来总结 NavigationUI 的核心逻辑。你看完应该就明白了:
说白了,NavigationUI 就是帮你把「导航」和「UI」之间的脏活累活全干了。你只需要配置好 nav_graph 和菜单资源,剩下的它自己搞定。我在好几个项目里都用这套方案,代码量至少减少了 40%,而且 bug 也少了很多。
最后提醒一句:NavigationUI 虽然方便,但它不是万能的。如果你的 UI 逻辑特别复杂(比如同一个页面有多个不同的 Toolbar 状态),那还是得自己写一些定制代码。不过对于 90% 的常规场景,它已经足够好用了。