基于嵌套导航图的拱形导航组件

我有一个案例,并希望实现它的拱导航组件。例如,我有2个导航图(主图和嵌套图)。我可以从嵌套调用主图吗? enter image description here

41397 次浏览

实际上是在工作, 使用

val host: NavHostFragment? = (childFragmentManager.findFragmentById(R.id.main_app_fragment_container)  as NavHostFragment?)

我可以从主片段导航

关键是获得正确的 NavController,以便在正确的图中导航。 让我们以这个场景为例:

MainActivity
|- MainNavHost
|- NavBarFragment
|  |- NestedNavHost
|  |  |-NestedContentFragment1
|  |  |-NestedContentFragment2
|  |
|  |- BottomNavigationView
|
|- LoginFragment

主图和嵌套图在单独的 xml 文件中: 据我所知,这是必需的,因为导航针对不同的布局区域,所以它们需要两个不同的 NavHost。每个 Navhost都需要通过 id 引用它的图,这要求它们位于不同的资源文件中。

关键在于,要在特定的图中导航,我们必须获得对右图所有者的引用: 为此,在调用 Navigation.findNavController(view)时,view参数是至关重要的。

医生说

NavHostFragments 在其视图子树的根注册它们的导航控制器,这样任何后代都可以通过导航帮助器类的方法获得控制器实例

例如,如果在 NavBarFragment中我们写

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
navController = Navigation.findNavController(view)
}

这里的 viewNestedNavHost家长(即嵌套的 NavHostFragment) ,而不是后代,这意味着 findNavController将在树的上游搜索并返回 MainNavHostNavController

如果我们写

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val nestedNavHostFragment = childFragmentManager.findFragmentById(R.id.nestedNavHostFragment) as? NavHostFragment
navController = nestedNavHostFragment?.navController
}

其中 nestedNavHostFragment是布局中 FragmentContainerView的 id,我们得到了正确的 NestedNavHost的引用。请注意使用的是 childFragmentManager,而不是 parentFragmentManager

如果您仍然使用不推荐的 xml <fragment>标记,可以编写

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val fragmentContainer = view.findViewById<View>(R.id.nestedNavHostFragment)
navController = Navigation.findNavController(fragmentContainer)
}

其中 nestedNavHostFragment<fragment>标记的 id。现在我们得到了对正确 NestedNavHost的引用,因为我们传递给 findNavController的视图属于 NestedNavHost的子树。

类似地,如果您需要从 NestedContentFragment内部获得对主 NavController的引用,我们可以这样做:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// we can get the innermost NavController using this view,
// because we are inside its subtree:
nestedNavController = Navigation.findNavController(view)


// we can find the outer NavController passing the owning Activity
// and the id of a view associated to that NavController,
// for example the NavHostFragment id:
mainNavController = Navigation.findNavController(activity!!, R.id.mainNavHostFragment)
}

实际上,您可以使用 全球行动从嵌套的导航图目的地导航到主导航图目的地。

在主导航图 (在下图中突出显示)中创建一个从嵌套导航图到所需目的地的全局操作

例如:

nav graph

<navigation android:id="@+id/main_nav_graph"
... >
<fragment android:id="@+id/fragStart" .../>
<fragment .../>
<fragment .../>


<navigation  android:id="@+id/nested_nav_graph">
...


<!-- Global Action -->
<action
android:id="@+id/action_global_start"
app:destination="@id/fragStart" />
</navigation>


</navigation>

导航到主图的目的地使用

findNavController().navigate(R.id.action_global_start)

我找到了一个临时解决方案来解决内部 NavController 被覆盖的问题。 您可以使用定制的 NavHostFragment,它为您提供所需的 NavController。 我的代码:

<androidx.fragment.app.FragmentContainerView
...
android:name="MyNavHostFragment"
app:defaultNavHost="false"
app:navGraph="@navigation/inner_nav">
...
</androidx.fragment.app.FragmentContainerView>

...

class MyNavHostFragment: NavHostFragment() {


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MainFragment.innerNavController = navController
}
}

...

class MainFragment : Fragment() {
companion object{
lateinit var innerNavController: NavController
}


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val bottomNavigationView =
view!!.findViewById<BottomNavigationView>(R.id.bottom_navigation_view)
bottomNavigationView.setupWithNavController(innerNavController)
}
}

我根据 Devrocca 提供的信息创建了一个答案。这是一个完整的答案,从头开始,如果有人需要,我没有跳过任何东西。

Main Fragment of Navigation

这是用于导航的主要片段。相机是没有任何嵌套图形的直接目的地,仪表板有它自己的嵌套图形,但它被添加到相同的回栈相机片段被添加。家里有3个碎片和它自己的导航主机

MainActivity
|- MainNavHost
|- HomeNavHostFragment
|  |- NestedNavHost
|     |-HomeFragment1
|     |-HomeFragment2
|     |-HomeFragment3
|
|- nav_graph_dashboard
|
|- CameraFragment

这是导航文件

主导航 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/main_dest">


<!-- MainFragment-->
<fragment
android:id="@+id/main_dest"
android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.MainFragment"
android:label="MainFragment"
tools:layout="@layout/fragment_main">


<!-- Camera -->
<action
android:id="@+id/action_main_dest_to_cameraFragment"
app:destination="@id/cameraFragment" />


<!-- Home NavGraph -->
<action
android:id="@+id/action_main_dest_to_nav_graph_home"
app:destination="@id/nav_graph_home" />


<!-- Dashboard  NavGraph-->
<action
android:id="@+id/action_main_dest_to_nav_graph_dashboard"
app:destination="@id/nav_graph_dashboard" />


</fragment>


<!-- Camera -->
<fragment
android:id="@+id/cameraFragment"
android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.CameraFragment"
android:label="CameraFragment" />




<!-- Home-->
<include app:graph="@navigation/nav_graph_home" />


<!-- Dashboard-->
<include app:graph="@navigation/nav_graph_dashboard" />




<!-- Global Action Start -->
<action
android:id="@+id/action_global_start"
app:destination="@id/main_dest"
app:popUpTo="@id/main_dest"
app:popUpToInclusive="true" />




</navigation>

仪表板嵌套导航图

<?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/dashboard_dest">


<fragment
android:id="@+id/dashboard_dest"
android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.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.tutorial1_3navigation_nestednavhost.blankfragment.DashboardFragment2"
android:label="DashboardFragment2"
tools:layout="@layout/fragment_dashboard2">
</fragment>


</navigation>

以及嵌套的导航图,它有自己的 NavHost nav _ graph _ home

<?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_home"
app:startDestination="@id/home_dest">


<fragment
android:id="@+id/home_dest"
android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.HomeNavHostFragment"
android:label="HomeHost"
tools:layout="@layout/fragment_home_navhost" />


<fragment
android:id="@+id/homeFragment1"
android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.HomeFragment1"
android:label="HomeFragment1"
tools:layout="@layout/fragment_home1">
<action
android:id="@+id/action_homeFragment1_to_homeFragment2"
app:destination="@id/homeFragment2" />
</fragment>


<fragment
android:id="@+id/homeFragment2"
android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.HomeFragment2"
android:label="HomeFragment2"
tools:layout="@layout/fragment_home2">
<action
android:id="@+id/action_homeFragment2_to_homeFragment3"
app:destination="@id/homeFragment3" />
</fragment>


<fragment
android:id="@+id/homeFragment3"
android:name="com.smarttoolfactory.tutorial1_3navigation_nestednavhost.blankfragment.HomeFragment3"
android:label="HomeFragment3"
tools:layout="@layout/fragment_home3" />


</navigation>

布局,我只添加必要的,其他是简单的布局与按钮,我添加样品项目与其他导航组件样品包括链接。

MainActivity




<?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.fragment.app.FragmentContainerView
android:id="@+id/main_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"


app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>


</androidx.constraintlayout.widget.ConstraintLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>


</layout>

主要片段,这是第一个片段,显示在图像中用作开始的主要导航

<?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:id="@+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">


<Button
android:id="@+id/btnDestCam"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Destination Camera"
app:layout_constraintBottom_toTopOf="@+id/btnNavGraphHome"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintRight_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<Button
android:id="@+id/btnNavGraphHome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nested NavHost Graph Home"
app:layout_constraintBottom_toTopOf="@+id/btnNavGraphDashboard"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintRight_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnDestCam" />


<Button
android:id="@+id/btnNavGraphDashboard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nested Graph Dashboard"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintRight_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnNavGraphHome" />




</androidx.constraintlayout.widget.ConstraintLayout>




</layout>

包含用于家庭导航的内部 NavHostFragment的布局

<?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"
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>

MainActivity 是用来检查主导航回栈的,重要的是

当你在主导航中导航它的 child FragmentManager 时,即使你只有一个,它也不会更新

class MainActivity : AppCompatActivity() {


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


// Get NavHostFragment
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.main_nav_host_fragment)


// ChildFragmentManager of NavHostFragment
val navHostChildFragmentManager = navHostFragment?.childFragmentManager


navHostChildFragmentManager?.addOnBackStackChangedListener {


val backStackEntryCount = navHostChildFragmentManager.backStackEntryCount
val fragments = navHostChildFragmentManager.fragments
}
}
}

包含 Home 导航主机的片段

class HomeNavHostFragment : BaseDataBindingFragment<FragmentHomeNavhostBinding>() {
override fun getLayoutRes(): Int = R.layout.fragment_home_navhost


private var navController: NavController? = null


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


val nestedNavHostFragment =
childFragmentManager.findFragmentById(R.id.nested_nav_host_fragment) as? NavHostFragment
navController = nestedNavHostFragment?.navController


navController?.navigate(R.id.homeFragment1)


listenBackStack()
}


private fun listenBackStack() {


// Get NavHostFragment
val navHostFragment =
childFragmentManager.findFragmentById(R.id.nested_nav_host_fragment)


// ChildFragmentManager of the current NavHostFragment
val navHostChildFragmentManager = navHostFragment?.childFragmentManager


navHostChildFragmentManager?.addOnBackStackChangedListener {


val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount
val fragments = navHostChildFragmentManager!!.fragments


Toast.makeText(
requireContext(),
"HomeNavHost backStackEntryCount: $backStackEntryCount, fragments: $fragments",
Toast.LENGTH_SHORT
).show()
}




val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {


val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount


Toast.makeText(
requireContext(),
"HomeNavHost backStackEntryCount: $backStackEntryCount",
Toast.LENGTH_SHORT
).show()




if (backStackEntryCount == 1) {
OnBackPressedCallback@ this.isEnabled = false
requireActivity().onBackPressed()
} else {
navController?.navigateUp()
}
}
}


requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)


}
}

有一件事我不知道它是否改进了图形或代码嵌套 NavHostFragment

如果您设置的开始目的地是 nav _ graph _ home HomeFragment1而不是 HomeNavHostFragment,那么它将作为指示板工作,忽略嵌套的 NavHost 并添加到片段的主回栈中。

因为您位于任何主片段的内部 NavHostFragmentfindNavController ()中,所以返回内部片段

class HomeFragment3 : BaseDataBindingFragment<FragmentHome3Binding>() {
override fun getLayoutRes(): Int = R.layout.fragment_home3


private var count = 0


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


dataBinding.btnIncrease.setOnClickListener {
dataBinding.tvTitle.text = "Count: ${count++}"
}




val mainNavController =
Navigation.findNavController(requireActivity(), R.id.main_nav_host_fragment)


dataBinding.btnGoToStart.setOnClickListener {


// 🔥Using destination belong to main_nav_host with nested navHost causes app to crash
//            findNavController().navigate(R.id.action_global_start)


mainNavController.navigate(R.id.action_global_start)/**/
}
}
}

您还可以使用全局操作,但是不需要这样做,因为如果不使用 OnBackPressed,内部导航主机中的返回导航会直接将您移回主导航。

如果您感兴趣,可以链接到 完整的例子和其他导航组件示例。

我们可以通过找到根导航主机控制器,然后在根导航主机控制器中导航来实现

val Fragment.findRootNavHost: NavController?
get() = this.activity?.let {
Navigation.findNavController(it, your_root_fragment_id)
}


findRootNavHost?.navigate(`your_destination_fragment_id`)

请检查 中等文章链接 Github 同样的回购协议