【问题标题】:How to implement a ViewPager with BottomNavigationView using new Navigation Architecture Component?如何使用新的导航架构组件实现带有 BottomNavigationView 的 ViewPager?
【发布时间】:2019-03-08 20:59:30
【问题描述】:

我有一个带有BottomNavigationViewViewPager 的应用程序。 如何使用新的“导航架构组件”来实现它?

最佳做法是什么?

非常感谢

【问题讨论】:

  • 你不能因为viewpager有不同的后栈!另一方面,您可以拥有一个单独的nested_graph,并从 view_pager Fragment 中导航到该nested_graph。

标签: android android-architecture-components


【解决方案1】:

我们可以使用底部导航组件和NavigationGraph轻松实现。

您应该为每个底部导航菜单创建相应的片段

nav_graph.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"
        app:startDestination="@id/actionHome">

        <fragment
            android:id="@+id/actionHome"
            android:name="com.sample.demo.fragments.Home"
            android:label="fragment_home"
            tools:layout="@layout/fragment_home">
            <action
                android:id="@+id/toExplore"
                app:destination="@id/actionExplore" />
        </fragment>
        <fragment
            android:id="@+id/actionExplore"
            android:name="com.sample.demo.fragments.Explore"
            android:label="fragment_explore"
            tools:layout="@layout/fragment_explore" />
        <fragment
            android:id="@+id/actionBusiness"
            android:name="com.sample.demo.fragments.Business"
            android:label="fragment_business"
            tools:layout="@layout/fragment_business" />
        <fragment
            android:id="@+id/actionProfile"
            android:name="com.sample.demo.fragments.Profile"
            android:label="fragment_profile"
            tools:layout="@layout/fragment_profile" />

    </navigation>

每个 Navigation Fragment ID 和底部导航菜单项 ID 应该相同。 例如这里

 <fragment
  android:id="@+id/actionBusiness"
 android:name="com.sample.demo.fragments.Business"
                android:label="fragment_business"
                tools:layout="@layout/fragment_business" />

底部导航菜单navigation.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:id="@+id/actionExplore"
        android:icon="@drawable/ic_search_24dp"
        android:title="@string/explore" />

    <item
        android:id="@+id/actionBusiness"
        android:icon="@drawable/ic_business_24dp"
        android:title="@string/business" />

    <item
        android:id="@+id/actionProfile"
        android:icon="@drawable/ic_profile_24dp"
        android:title="@string/profile" />


</menu>

将 nav_graph.xml 设置为 activity_main.xml 中的占位符片段

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/gradient_bg"
    android:focusable="true"
    android:focusableInTouchMode="true"
    tools:context=".MainActivity"
    tools:layout_editor_absoluteY="25dp">

    <android.support.design.widget.BottomNavigationView
        android:id="@+id/navigation"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="@color/semi_grey"
        app:itemIconTint="@drawable/bottom_bar_nav_item"
        app:itemTextColor="@drawable/bottom_bar_nav_item"
        app:labelVisibilityMode="labeled"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/navigation" />

    <include
        android:id="@+id/appBarLayout"
        layout="@layout/app_bar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <fragment
        android:id="@+id/mainNavigationFragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:paddingBottom="@dimen/activity_horizontal_margin"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@+id/navigation"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
        app:navGraph="@navigation/nav_graph" />

</android.support.constraint.ConstraintLayout>

在此处将导航图映射到片段 app:navGraph="@navigation/nav_graph"

之后在MainActivity.java中实现导航图和bottomNavigation组件

 BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
        NavController navController = Navigation.findNavController(this, R.id.mainNavigationFragment);
        NavigationUI.setupWithNavController(navigation, navController); 

干杯!!!

【讨论】:

  • 问题在于使用拱形导航的 ViewPager 实现。
  • 我有同样的问题,我无法使用标签实现导航架构到视图寻呼机
【解决方案2】:

更新(21 年 6 月 15 日):

从 Navigation 组件版本 2.4.0-alpha01 开始,支持开箱即用的多个返回堆栈。根据文档,如果您将NavigationViewBottomNavigationView 与导航组件一起使用,那么多个后退堆栈应该可以工作,而无需对先前的实现进行任何代码更改。

作为此更改的一部分,onNavDestinationSelected()、BottomNavigationView.setupWithNavController() 和 NavigationView.setupWithNavController() 的 NavigationUI 方法现在可以自动保存和恢复弹出目的地的状态,无需任何代码更改即可支持多个返回堆栈。当使用带有 Fragments 的导航时,这是与多个返回堆栈集成的推荐方式。

原答案:

BottomNavigationView 与 Navigation Arch 组件的默认实现不适合我。单击选项卡时,它会根据导航图从头开始。

我需要在屏幕底部有 5 个选项卡,并且每个选项卡都有一个单独的后退堆栈。这意味着在标签之间切换时,您将始终返回与离开前完全相同的状态(如在 Instagram 中)。

我的做法如下:

  1. ViewPagerBottomNavigationView 放入activity_main.xml
  2. MainActivity.kt 中将OnNavigationItemSelectedListener 设置为BottomNavigationView
  3. 为每个选项卡创建单独的容器片段(它们将是每个选项卡的起点)
  4. 在容器片段的 xml 中包含 NavHostFragment
  5. 在每个 Container 片段中实现 Navigation Arch 组件的必要代码。
  6. 为每个选项卡创建一个图表

注意:每个图表都可以相互交互。

这里重要的一点是,我们将工具栏不是放在活动中,而是放在容器片段中。然后我们在工具栏本身上调用setupWithNavController(),而不将其设置为supportActionBar。这样工具栏标题将自动更新,Back/Up按钮将自动管理。

结果:

  • ViewPager 存储了每个选项卡的状态。
  • 不用担心片段事务。
  • SafeArgsDeepLinking 按预期工作。
  • 我们可以完全控制BottomNavigationManagerViewPager(即我们可以实现OnNavigationItemReselectedListener 并决定在弹出堆栈之前将当前选项卡中的列表滚动到顶部)。

代码:

activity_main.xml

<LinearLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/main_view_pager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/main_bottom_navigation_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="?android:attr/windowBackground"
        app:menu="@menu/navigation" />

</LinearLayout>

MainActivity.kt

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {

    private lateinit var viewPagerAdapter: ViewPagerAdapter

    private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
        when (item.itemId) {
            R.id.navigation_tab_1 -> {
                main_view_pager.currentItem = 0
                return@OnNavigationItemSelectedListener true
            }
            R.id.navigation_tab_2 -> {
                main_view_pager.currentItem = 1
                return@OnNavigationItemSelectedListener true
            }
        }
        false
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
        main_view_pager.adapter = viewPagerAdapter
        
        main_bottom_navigation_view.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
    }
}

ViewPagerAdapter.kt

class ViewPagerAdapter(fm: FragmentManager) : FragmentPagerAdapter(fm) {

    override fun getItem(position: Int): Fragment {
        return when (position) {
            0 -> Tab1ContainerFragment()
            else -> Tab2ContainerFragment()
        }
    }

    override fun getCount(): Int {
        return 2
    }
}

fragment_tab_1_container.xml

<RelativeLayout 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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Tab1ContainerFragment">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/tab_1_toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:theme="@style/ThemeOverlay.AppCompat.Dark" />

    <fragment
        android:id="@+id/tab_1_nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/navigation_graph_tab_1" />

</RelativeLayout>

Tab1ContainerFragment.kt

class Tab1ContainerFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_tab_1_container, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val toolbar = view.findViewById<Toolbar>(R.id.tab_1_toolbar)

        val navHostFragment = childFragmentManager.findFragmentById(R.id.tab_1_nav_host_fragment) as NavHostFragment? ?: return

        val navController = navHostFragment.navController

        val appBarConfig = AppBarConfiguration(navController.graph)

        toolbar.setupWithNavController(navController, appBarConfig)
    }
}

我们可以根据需要创建任意数量的导航图:

但我们需要为每个选项卡创建一个单独的图表:

<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/navigation_graph_tab_1"
    app:startDestination="@id/tab1StartFragment">

    <fragment
        android:id="@+id/tab1StartFragment"
        android:name="com.marat.android.bottomnavigationtutorial.Tab1StartFragment"
        android:label="fragment_tab_1_start"
        tools:layout="@layout/fragment_tab_1_start">
        <action
            android:id="@+id/action_tab_1_to_content"
            app:destination="@id/navigation_graph_content" />
    </fragment>

    <include app:graph="@navigation/navigation_graph_content" />
</navigation>

此处的起始目标片段是您希望在选项卡中显示为第一个屏幕的任何片段。

【讨论】:

  • 我已经添加了上面提到的所有文件,并且 TabContainerFragment 成功显示了我的 TabStart 片段。但是我在包含内容图后面临问题。我在下面的代码中崩溃以从容器片段导航到内容片段。 val navigateTOHome2 = Home1FragmentDirections.actionHome1FragmentToHome2Fragment(); findNavController().navigate(navigateTOHome2) (NavController() 找不到)但是如果我将嵌套导航代码移动到容器导航,它工作正常。万一它工作时,按下后退按钮后退弹不起作用。
  • 没有更多信息很难理解这个问题。但是您不应该从容器片段导航到内容片段。相反,您提供的代码应该放在 TabStartFragment 内。
  • 当 Activity 直接托管 ViewPager 并且 tab1Container 托管 NavHostFragment 然后使用 app:defaultNavHost="true" 设备后退按钮不会被拦截。怎么办?
  • @MuhammadMaqsood 这种方法非常有效:stackoverflow.com/a/54522734/5123022
  • 我正在尝试遵循此解决方案,但我在使用选项菜单时遇到了一些问题。我有 2 个选项卡,它们的每个起始片段都有菜单 (setHasOptionsMenu(true))。工具栏/操作栏应该只显示当前在选项卡中可见的片段的菜单,但事实并非如此。两个片段的菜单会同时显示,即使对于尚不可见的片段也是如此。我一直在试图弄清楚如何解决这个问题,但我现在没有想法了。如何解决/解决这个问题?
【解决方案3】:

我已经用 viewpager 实现了 Android Arch Navigations。请看一看。欢迎任何改进。一起学习吧。

https://github.com/Maqsood007/AndroidJetpack/tree/master/ArchNavViewPagerImpl

【讨论】:

    【解决方案4】:

    我的一个解决方案是将 ViewPager 中的片段留在导航之外,并直接设置页面片段上的操作,就好像这些页面是主机一样。 为了更好地解释它:

    假设你在 Fragment A 中,ViewPager 是 Fragment B 然后你尝试从 B 导航到 C

    在片段 B 中,使用 ADirections 类和从 A 到 C 的动作。findNavHost().navigateTo(ADirections.ActionFromAtoC)

    【讨论】:

      【解决方案5】:

      我写过a related article on this regarding view pagers,特别关注标签式的主从片段,但同样的逻辑也适用于常规的ViewPagers。代码是located here

      【讨论】:

        【解决方案6】:

        我有一个 MainFragment,它在 viewPager 中托管 Fragment A、Fragment B 和 Fragment C。

        我想从 Fragment B(由 MainFragment 中的 viewPager 托管)打开 Fragment D。

        所以我创建了一个从 MainFragment 到 Fragment D 的动作,并从 Fragment B 调用

        val direction = FragmentMainDirections.actionFragmentMainToFragmentD()
        findNavController().navigate(direction)
        

        有效。

        【讨论】:

        • 当用户单击后退按钮应用程序崩溃时不起作用..!!
        【解决方案7】:

        除了 Marat 的回答之外,为了让后退堆栈与每个片段中的后退按钮一起使用,您必须将其添加到您的容器片段 onViewCreated:

        val callback = object : OnBackPressedCallback(true) {
                    override fun handleOnBackPressed() {
                        if (!navHostFragment.navController.popBackStack()) {
                            isEnabled = false
                            activity?.onBackPressed()
                        }
                    }
                }
        activity?.onBackPressedDispatcher?.addCallback(this, callback)
        

        【讨论】:

          【解决方案8】:

          感谢@Marat - 他提供了很好的解决方案。 在我的情况下,我有 第二个 ViewPager 视图的列表/详细视图导航,并使用 全屏模式,没有任何操作\工具栏

          想评论一些时刻:

          1) 我可以很容易地为一页使用一个通用图表:

          <?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/page2Coordinator"
              app:startDestination="@id/Fragment_2Coordinator">
          
              <fragment
                  android:id="@+id/Fragment_2Coordinator"
                  android:name="my.app.Fragment_2Coordinator"
                  android:label="Fragment_2Coordinator">
                  <action
                      android:id="@+id/action_showList_2A"
                      app:destination="@id/Fragment_2A" />
              </fragment>
          
              <fragment
                  android:id="@+id/Fragment_2A"
                  android:name="my.app.Fragment_2A"
                  android:label="Fragment_2A">
          
                  <action
                      android:id="@+id/action_goToDetail_2B"
                      app:destination="@id/Fragment_2B" />
              </fragment>
          
              <fragment
                  android:id="@+id/Fragment_2B"
                  android:name="my.app.Fragment_2B"
                  android:label="Fragment_2B">
          
                  <action
                      android:id="@+id/action_backToList_2A"
                      app:destination="@id/Fragment_2A" />
              </fragment>
          </navigation>
          

          2) 无需使用 Fragment_2Coordinator.onViewCreated() 中的工具栏进行操作,只需使用图形中的操作进行导航(以防您不使用系统导航):

          override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
              super.onViewCreated(view, savedInstanceState)
              val navHostFragment = childFragmentManager.findFragmentById(R.id. tab_1_nav_host_fragment) as NavHostFragment? ?: return
              val navController = navHostFragment.navController
              navController.navigate(R.id.action_showList_2A)
          }
          

          3) 使用手机返回按钮提供从 2B 到 2A 的返回 - 转到活动:

          class MainActivity : AppCompatActivity() {
          
           .  .  .  .  . 
          
              override fun onBackPressed() {
          
                  val navController = findNavController(R.id.tab_1_nav_host_fragment)
          
                  when(navController.currentDestination?.id) {
                      R.id.Fragment_2B -> {
                          navController.navigate(R.id.action_backToList_2A)
                      }
                      else -> {
                          super.onBackPressed()
                      }
                  }
                  println()
              }
          }
          

          【讨论】:

            【解决方案9】:

            我创建了一个在 Activity 上有工具栏的示例,您还可以创建具有自己工具栏的 ViewPager 片段。它使用OnBackPressedCallback 进行后退导航,ViewModel 使用childFragmentManager 或嵌套片段设置当前NavControllerNavHostFragment,并通过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()
                        }
            
                    }
                }
            }
            

            嵌套导航需要注意的重要事项是

            1. 按下后退按钮时能够正确导航
            2. 仅从可见片段导航,如果未正确实现,则调用其他片段回调后按
            3. 旋转后仅将可见片段的后按设置为活动状态

            首先,你需要检查你是否是图表的起始目的地,因为你需要调用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)
            

            【讨论】:

            • 你能把示例应用发给我吗
            • 或者当我的片段没有工具栏时,你能指导我解决这个问题吗
            • Github 链接出现错误 404。能否请您更新参考链接
            • 当然,更新了link for this sample。对于包含其他样本的entire repo
            • @AbdullahJaved,您还可以查看此sample,它是一个工作应用程序,而不仅仅是一个小样本,除此之外,您还可以将动态导航模块片段作为 ViewPager2 和 BottomNavigationView 的基础跨度>
            猜你喜欢
            • 2019-04-27
            • 1970-01-01
            • 1970-01-01
            • 2023-03-29
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多