我创建了一个在 Activity 上有工具栏的示例,您还可以创建具有自己工具栏的 ViewPager 片段。它使用OnBackPressedCallback 进行后退导航,ViewModel 使用childFragmentManager 或嵌套片段设置当前NavController 和NavHostFragment,并通过viewLifeCycleOwner 尊重生命周期并在暂停时禁用回调并启用onResume。
导航和布局架构
MainActivity(Appbar + Toolbar + ViewPager2 + BottomNavigationView)
|
|- HomeNavHostFragment
| |- HF1 -> HF2 -> HF3
|
|- DashboardNavHostFragment
| |- DF1 -> DF2 -> DF3
|
|- NotificationHostFragment
|- NF1 -> NF2 -> NF3
首先,为ViewPager2的每个选项卡或片段创建一个导航图
nav_graph_home.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph_dashboard"
app:startDestination="@id/dashboardFragment1">
<fragment
android:id="@+id/dashboardFragment1"
android:name="com.smarttoolfactory.tutorial7_1bnw_viewpager2_nestednavigation.blankfragment.DashboardFragment1"
android:label="DashboardFragment1"
tools:layout="@layout/fragment_dashboard1">
<action
android:id="@+id/action_dashboardFragment1_to_dashboardFragment2"
app:destination="@id/dashboardFragment2" />
</fragment>
<fragment
android:id="@+id/dashboardFragment2"
android:name="com.smarttoolfactory.tutorial7_1bnw_viewpager2_nestednavigation.blankfragment.DashboardFragment2"
android:label="DashboardFragment2"
tools:layout="@layout/fragment_dashboard2">
<action
android:id="@+id/action_dashboardFragment2_to_dashboardFragment3"
app:destination="@id/dashboardFragment3" />
</fragment>
<fragment
android:id="@+id/dashboardFragment3"
android:name="com.smarttoolfactory.tutorial7_1bnw_viewpager2_nestednavigation.blankfragment.DashboardFragment3"
android:label="DashboardFragment3"
tools:layout="@layout/fragment_dashboard3" >
<action
android:id="@+id/action_dashboardFragment3_to_dashboardFragment1"
app:destination="@id/dashboardFragment1"
app:popUpTo="@id/dashboardFragment1"
app:popUpToInclusive="true" />
</fragment>
</navigation>
其他导航图与此相同
BottomNavigationView 的菜单
menu_bottom_nav.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_graph_home"
android:icon="@drawable/ic_baseline_home_24"
android:title="Home"/>
<item
android:id="@+id/nav_graph_dashboard"
android:icon="@drawable/ic_baseline_dashboard_24"
android:title="Dashboard"/>
<item
android:id="@+id/nav_graph_notification"
android:icon="@drawable/ic_baseline_notifications_24"
android:title="Notification"/>
</menu>
ViewPager2 适配器
class ActivityFragmentStateAdapter(fragmentActivity: FragmentActivity) :
FragmentStateAdapter(fragmentActivity) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> HomeNavHostFragment()
1 -> DashBoardNavHostFragment()
else -> NotificationHostFragment()
}
}
}
主要活动的布局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/bottom_nav"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/menu_bottom_nav" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
MainActivity 在我们更改标签时,都会监听BottomNavigationView 的项目变化和当前NavController 的变化,因为我们必须为每个标签设置Appbar 导航。
class MainActivity : AppCompatActivity() {
// private val appbarViewModel by viewModels<AppbarViewModel>()<AppbarViewModel>()
private val appbarViewModel:AppbarViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val dataBinding: ActivityMainBinding =
DataBindingUtil.setContentView(this, R.layout.activity_main)
val viewPager2 = dataBinding.viewPager
val bottomNavigationView = dataBinding.bottomNav
// Cancel ViewPager swipe
viewPager2.isUserInputEnabled = false
// Set viewpager adapter
viewPager2.adapter = ActivityFragmentStateAdapter(this)
// Listen bottom navigation tabs change
bottomNavigationView.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.nav_graph_home -> {
viewPager2.setCurrentItem(0, false)
return@setOnNavigationItemSelectedListener true
}
R.id.nav_graph_dashboard -> {
viewPager2.setCurrentItem(1, false)
return@setOnNavigationItemSelectedListener true
}
R.id.nav_graph_notification -> {
viewPager2.setCurrentItem(2, false)
return@setOnNavigationItemSelectedListener true
}
}
false
}
appbarViewModel.currentNavController.observe(this, Observer { navController ->
navController?.let {
val appBarConfig = AppBarConfiguration(it.graph)
dataBinding.toolbar.setupWithNavController(it, appBarConfig)
}
})
}
}
AppbarViewModel 只有一个MutableLiveData 来设置当前NavController。目的是使用 ViewModel 在 NavHost Fragment 中设置 ViewModel 并能够在 Activity 或其他 Fragment 中获取它。
class AppbarViewModel : ViewModel() {
val currentNavController = MutableLiveData<NavController?>()
}
具有 FragmentContainerView 的 NavHost 的布局,当我将工具栏放入这些片段并使用 FragmentContainerView 时出现错误,如果您使用带有导航的 appBar,请使用 fragment。
fragment_navhost_home.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nested_nav_host_fragment_home"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="false"
app:navGraph="@navigation/nav_graph_home"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
包含子片段和NavController的NavHost Fragment,其中3个相同,所以我只放一个
class HomeNavHostFragment : BaseDataBindingFragment<FragmentNavhostHomeBinding>() {
override fun getLayoutRes(): Int = R.layout.fragment_navhost_home
private val appbarViewModel by activityViewModels<AppbarViewModel>()
private var navController: NavController? = null
private val nestedNavHostFragmentId = R.id.nested_nav_host_fragment_home
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val nestedNavHostFragment =
childFragmentManager.findFragmentById(nestedNavHostFragmentId) as? NavHostFragment
navController = nestedNavHostFragment?.navController
// Listen on back press
listenOnBackPressed()
}
private fun listenOnBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}
override fun onResume() {
super.onResume()
callback.isEnabled = true
// Set this navController as ViewModel's navController
appbarViewModel.currentNavController.value = navController
}
override fun onPause() {
super.onPause()
callback.isEnabled = false
}
/**
* This callback should be created with Disabled because on rotation ViewPager creates
* NavHost fragments that are not on screen, destroys them afterwards but it might take
* up to 5 seconds.
*
* ### Note: During that interval touching back button sometimes call incorrect [OnBackPressedCallback.handleOnBackPressed] instead of this one if callback is **ENABLED**
*/
val callback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
// Check if it's the root of nested fragments in this navhost
if (navController?.currentDestination?.id == navController?.graph?.startDestination) {
Toast.makeText(requireContext(), "AT START DESTINATION ", Toast.LENGTH_SHORT)
.show()
/*
Disable this callback because calls OnBackPressedDispatcher
gets invoked calls this callback gets stuck in a loop
*/
isEnabled = false
requireActivity().onBackPressed()
isEnabled = true
} else {
navController?.navigateUp()
}
}
}
}
嵌套导航需要注意的重要事项是
- 按下后退按钮时能够正确导航
- 仅从可见片段导航,如果未正确实现,则调用其他片段回调后按
- 旋转后仅将可见片段的后按设置为活动状态
首先,你需要检查你是否是图表的起始目的地,因为你需要调用requireActivity().onBackPressed()来回调Activity,否则你会卡在HomeFragment1
如果您在调用 requireActivity().onBackPressed() 之前没有禁用回调,您会陷入循环,因为 onBackPressed 也会调用 Active 回调
如果您在当前 Fragment 不可见时不禁用 callback.isEnabled = false,则每个回调都会被调用
最后,我认为最重要的是如果您旋转设备
其他选项卡中的片段也由 viewPager 创建,然后在 3 到 5 后销毁,但不会调用它们的 onResume,这会导致其他回调调用 handleBackPressed 如果您创建对象:OnBackPressedCallback(true强>),使用
object : OnBackPressedCallback(false)
例如,如果回调处于活动状态,并且您在 HomeFragment3 打开时旋转设备,并且您在回调处于活动状态时触摸返回按钮
2020-06-28 13:23:42.722 I: ? HomeNavHostFragment #208670033 onCreate()
2020-06-28 13:23:42.729 I: ⏰ NotificationHostFragment #19727909 onCreate()
2020-06-28 13:23:42.826 I: ? HomeNavHostFragment #208670033 onViewCreated()
2020-06-28 13:23:42.947 I: ⏰ NotificationHostFragment #19727909 onViewCreated()
2020-06-28 13:23:42.987 I: ? HomeNavHostFragment #208670033 onResume()
2020-06-28 13:23:44.092 I: ⏰ NotificationHostFragment #19727909 handleOnBackPressed()
2020-06-28 13:23:44.851 I: ⏰ NotificationHostFragment #19727909 handleOnBackPressed()
2020-06-28 13:23:53.011 I: ⏰ NotificationHostFragment #19727909 onDestroyView()
2020-06-28 13:23:53.023 I: ⏰ NotificationHostFragment #19727909 onDestroy()
即使我在 HomeFragment3 可见时按了两次返回按钮,⏰ NotificationHostFragment #19727909 handleOnBackPressed() 也会被调用,因为 ViewPager 创建了也不可见的片段并在之后销毁它们。我的例子用了 10 秒,你也可以试试看。
编辑:建议在 ViewPager 2 的每个片段中使用 onBackPressedDispatcher 而不是 FragmentStateAdapter 中的以下片段,它将屏幕上的活动片段设置为 主要导航片段强>。
/**
* FragmentStateAdapter to add ability to set primary navigation fragment
* which lets fragment visible to be navigable when back button is pressed using
* [FragmentStateAdapter.FragmentTransactionCallback] in [ViewPager2].
*
* * ? Create FragmentStateAdapter with viewLifeCycleOwner instead of Fragment to make sure
* that it lives between [Fragment.onCreateView] and [Fragment.onDestroyView] while [View] is alive
*
* * https://stackoverflow.com/questions/61779776/leak-canary-detects-memory-leaks-for-tablayout-with-viewpager2
*/
abstract class NavigableFragmentStateAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager, lifecycle) {
private val fragmentTransactionCallback =
object : FragmentStateAdapter.FragmentTransactionCallback() {
override fun onFragmentMaxLifecyclePreUpdated(
fragment: Fragment,
maxLifecycleState: Lifecycle.State
) = if (maxLifecycleState == Lifecycle.State.RESUMED) {
// This fragment is becoming the active Fragment - set it to
// the primary navigation fragment in the OnPostEventListener
OnPostEventListener {
fragment.parentFragmentManager.commitNow {
setPrimaryNavigationFragment(fragment)
}
}
} else {
super.onFragmentMaxLifecyclePreUpdated(fragment, maxLifecycleState)
}
}
init {
// Add a FragmentTransactionCallback to handle changing
// the primary navigation fragment
registerFragmentTransactionCallback()
}
fun registerFragmentTransactionCallback() {
registerFragmentTransactionCallback(fragmentTransactionCallback)
}
fun unregisterFragmentTransactionCallback() {
unregisterFragmentTransactionCallback(fragmentTransactionCallback)
}
}
这里是link for full sample。你也可以把Toolbar放到每个navHost的fragment里面,简单一点。
您使用工具栏在 NavHost 片段中调用的内容
val appBarConfig = AppBarConfiguration(navController!!.graph)
dataBinding.toolbar.setupWithNavController(navController!!, appBarConfig)