我可以在导航组件中使用多个 NavHostFragments 吗?
Can I use multiple NavHostFragments in Navigation Component?
如果下面这段话你理解有困难,请看我做的流程图
我目前正在制作一个包含 3 个顶级目的地的笔记应用程序。其中一个顶级目的地(NotesList)显示了用户创建的笔记列表。 NotesList 有一个过滤器按钮,它会弹出一个带有 FilterMenu 目标的底部模式 sheet。 FilterMenu 有一个搜索按钮,单击该按钮会将 sheet 的内容替换为搜索目标,还有一个名为标签的按钮,单击该按钮会将 sheet 的内容替换为包含列表的片段与所有笔记关联的标签(TagList 目的地)。
蓝色的一切都是顶级目的地。紫色的所有内容都出现在模态 sheet.
中
FilterMenu、Search 和 TagList 以模态显示 sheet。这意味着 NotesList 包含 这些片段并且不会被它们替换。它们存在于比 NotesList 更小的屏幕区域中。如果我使用导航,片段将相互替换。
我可以使用两个 NavHost 吗?一个用于顶级目的地,一个用于模式 sheet 中的内容?如果是这样,我将如何实施它?如果不是,在这种情况下建议做什么?
创建两个 navHostFragment 的简单方法是创建另一个 navigation.xml 文件。
例如,在我的应用程序中,我有两个 navHostsFragments。
我为导航流程定义了第一个,因此当用户进入应用程序时,他会转到登录片段,即 navHostFragment。
用户登录后,他将转到包含我的新 navHostFragment 的 mainActivity。
每当我启动一个包含我的新 navHostFragment 的新 activity 时就是这样。
希望对您有所帮助
您可以创建两个导航图来实现您想要的行为。一个用于顶级目的地,另一个用于模态 sheet。他们需要独立,彼此之间没有任何联系。您不能只使用一个导航图,因为 "navigation surface" 是不同的导航图。对于主导航,它是 activity,对于模态底部 sheet,它是底部 sheets window(在 BottomSheetDialogFragment 的情况下,实际上是不同的 window).
理论上这很容易实现:
main_nav.xml
包含 Settings
、NoteList
和 Trash
filter_nav.xml
包含 FilterMenu
、Search
和 TagList
如果您不想在顶层进行后退导航,您甚至可以在没有导航控制器的情况下使用片段事务进行顶层导航。
所以基本上你需要一个 (BottomSheet)DialogFragment
,它需要一个独立于 main/other NavHost
的单独的 NavHost
。您可以通过以下 class:
实现此目的
dialog_fragment_modal_bottom_sheet.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/filterNavHost"/>
ModalBottomSheetDialogFragment.kt
class ModalBottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.dialog_fragment_modal_bottom_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// We can't inflate the NavHostFragment from XML because it will crash the 2nd time the dialog is opened
val navHost = NavHostFragment()
childFragmentManager.beginTransaction().replace(R.id.filterNavHost, navHost).commitNow()
navHost.navController.setGraph(R.navigation.filter_nav)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
// Normally the dialog would close on back press. We override this behaviour and check if we can navigate back
// If we can't navigate back we return false triggering the default implementation closing the dialog
setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
view?.findNavController()?.popBackStack() == true
} else {
false
}
}
}
}
}
我们这里有两个技巧:
我们需要手动创建 NavHost
片段。如果我们直接输入XML,第二次打开对话框会崩溃,因为ID已经被使用
我们需要覆盖对话框的后退导航。对话框是 activity 之上的单独 window,因此不会调用 Activity
的 onBackPressed()
。相反,我们添加了一个 OnKeyListener
并且当后退按钮被释放时 (ACTION_UP
) 我们检查 NavController
是否可以弹出返回堆栈(返回)。如果它可以弹出返回堆栈,我们 return true 并因此消耗返回事件。对话框保持打开状态,NavController
后退一步。如果它已经在起点,对话框将关闭,因为我们 return false.
您现在可以在对话框中创建嵌套图而不用关心外部图。要显示带有嵌套图的对话框,请使用:
val dialog = ModalBottomSheetDialogFragment()
dialog.show(childFragmentManager, "filter-menu")
您也可以在 main_nav
中添加 ModalBottomSheetDialogFragment
作为 <dialog>
目的地,不过我没有对此进行测试。此功能目前仍处于 alpha 阶段,并在导航 2.1.0-alpha03 中引入。因为这仍处于 alpha 阶段,API 可能会改变,我个人会使用上面的代码来显示对话框。一旦超出 alpha/beta,使用 main_nav.xml
中的目的地应该是首选方式。从用户的角度来看,显示对话框的不同方式没有区别。
我使用您的导航结构创建了一个示例应用程序 here on GitHub. It has working back navigation on both levels with the two independent graphs. You can see it working here on Youtube。我在主导航中使用了底部栏,但您可以将其替换为抽屉。
如果下面这段话你理解有困难,请看我做的流程图
我目前正在制作一个包含 3 个顶级目的地的笔记应用程序。其中一个顶级目的地(NotesList)显示了用户创建的笔记列表。 NotesList 有一个过滤器按钮,它会弹出一个带有 FilterMenu 目标的底部模式 sheet。 FilterMenu 有一个搜索按钮,单击该按钮会将 sheet 的内容替换为搜索目标,还有一个名为标签的按钮,单击该按钮会将 sheet 的内容替换为包含列表的片段与所有笔记关联的标签(TagList 目的地)。
蓝色的一切都是顶级目的地。紫色的所有内容都出现在模态 sheet.
中FilterMenu、Search 和 TagList 以模态显示 sheet。这意味着 NotesList 包含 这些片段并且不会被它们替换。它们存在于比 NotesList 更小的屏幕区域中。如果我使用导航,片段将相互替换。
我可以使用两个 NavHost 吗?一个用于顶级目的地,一个用于模式 sheet 中的内容?如果是这样,我将如何实施它?如果不是,在这种情况下建议做什么?
创建两个 navHostFragment 的简单方法是创建另一个 navigation.xml 文件。
例如,在我的应用程序中,我有两个 navHostsFragments。
我为导航流程定义了第一个,因此当用户进入应用程序时,他会转到登录片段,即 navHostFragment。
用户登录后,他将转到包含我的新 navHostFragment 的 mainActivity。
每当我启动一个包含我的新 navHostFragment 的新 activity 时就是这样。
希望对您有所帮助
您可以创建两个导航图来实现您想要的行为。一个用于顶级目的地,另一个用于模态 sheet。他们需要独立,彼此之间没有任何联系。您不能只使用一个导航图,因为 "navigation surface" 是不同的导航图。对于主导航,它是 activity,对于模态底部 sheet,它是底部 sheets window(在 BottomSheetDialogFragment 的情况下,实际上是不同的 window).
理论上这很容易实现:
main_nav.xml
包含Settings
、NoteList
和Trash
filter_nav.xml
包含FilterMenu
、Search
和TagList
如果您不想在顶层进行后退导航,您甚至可以在没有导航控制器的情况下使用片段事务进行顶层导航。
所以基本上你需要一个 (BottomSheet)DialogFragment
,它需要一个独立于 main/other NavHost
的单独的 NavHost
。您可以通过以下 class:
dialog_fragment_modal_bottom_sheet.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/filterNavHost"/>
ModalBottomSheetDialogFragment.kt
class ModalBottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.dialog_fragment_modal_bottom_sheet, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// We can't inflate the NavHostFragment from XML because it will crash the 2nd time the dialog is opened
val navHost = NavHostFragment()
childFragmentManager.beginTransaction().replace(R.id.filterNavHost, navHost).commitNow()
navHost.navController.setGraph(R.navigation.filter_nav)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
// Normally the dialog would close on back press. We override this behaviour and check if we can navigate back
// If we can't navigate back we return false triggering the default implementation closing the dialog
setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
view?.findNavController()?.popBackStack() == true
} else {
false
}
}
}
}
}
我们这里有两个技巧:
我们需要手动创建
NavHost
片段。如果我们直接输入XML,第二次打开对话框会崩溃,因为ID已经被使用我们需要覆盖对话框的后退导航。对话框是 activity 之上的单独 window,因此不会调用
Activity
的onBackPressed()
。相反,我们添加了一个OnKeyListener
并且当后退按钮被释放时 (ACTION_UP
) 我们检查NavController
是否可以弹出返回堆栈(返回)。如果它可以弹出返回堆栈,我们 return true 并因此消耗返回事件。对话框保持打开状态,NavController
后退一步。如果它已经在起点,对话框将关闭,因为我们 return false.
您现在可以在对话框中创建嵌套图而不用关心外部图。要显示带有嵌套图的对话框,请使用:
val dialog = ModalBottomSheetDialogFragment()
dialog.show(childFragmentManager, "filter-menu")
您也可以在 main_nav
中添加 ModalBottomSheetDialogFragment
作为 <dialog>
目的地,不过我没有对此进行测试。此功能目前仍处于 alpha 阶段,并在导航 2.1.0-alpha03 中引入。因为这仍处于 alpha 阶段,API 可能会改变,我个人会使用上面的代码来显示对话框。一旦超出 alpha/beta,使用 main_nav.xml
中的目的地应该是首选方式。从用户的角度来看,显示对话框的不同方式没有区别。
我使用您的导航结构创建了一个示例应用程序 here on GitHub. It has working back navigation on both levels with the two independent graphs. You can see it working here on Youtube。我在主导航中使用了底部栏,但您可以将其替换为抽屉。