ngu*_*ngu 19 android android-navigation android-architecture-components android-jetpack android-architecture-navigation
对于新的应用程序,我使用Jetpack导航库来实现正确的后退导航.第一级导航是导航抽屉,如文档中所述,可以使用jetpack导航.但是ViewPager和TabLayout实现了另一个导航级别.由TabLayout切换的片段包含额外的线性导航层次结构.但是,在Jetpack Navigation中似乎不支持ViewPager/TabLayout.必须实现FragmentPagerAdapter,并在切换选项卡时结束托管后端堆栈.顶级导航与每个选项卡内的导航之间存在脱节.有没有办法让这个工作与Jetpack导航?
您实现应用栏导航的方式会改变您的实现。如果您希望使用从页面到详细信息的导航,它使用与主要 NavHost 片段相同的 fragmentManager。这就像详细介绍片段/活动。
主页、仪表板和通知有自己的图表,因此它们可以打开子片段,而登录片段属于主导航图,因此它将片段作为详细信息片段打开。
此实现需要NavHostFragment在片段或MainActivity.
布局
活动_main.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.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">
<fragment
android:id="@+id/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>
Run Code Online (Sandbox Code Playgroud)
截至目前androidx.fragment.app.FragmentContainerView崩溃,并appbar导航,所以使用的片段,如果你遇到navController未找到错误
fragment_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:background="@color/colorPrimary"
app:tabTextColor="#fff"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
</androidx.constraintlayout.widget.ConstraintLayout>
Run Code Online (Sandbox Code Playgroud)
ViewPager2 的 Fragment 有 NavHostFragment,只添加一个,其他的和这个布局一样,除了app:navGraph="@navigation/nav_graph_home"自己的图。
fragment_nav_host_home.xml
<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>
Run Code Online (Sandbox Code Playgroud)
其他片段没有什么特别之处,跳过它们,如果您有兴趣,我添加了完整示例和其他导航组件示例的链接。
导航图
主导航图,nav_graph.xml
<!-- MainFragment-->
<fragment
android:id="@+id/main_dest"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.blankfragment.MainFragment"
android:label="MainFragment"
tools:layout="@layout/fragment_main">
<!-- Login -->
<action
android:id="@+id/action_main_dest_to_loginFragment2"
app:destination="@id/loginFragment2" />
</fragment>
<!-- Global Action Start -->
<action
android:id="@+id/action_global_start"
app:destination="@id/main_dest"
app:popUpTo="@id/main_dest"
app:popUpToInclusive="true" />
<!-- Login -->
<fragment
android:id="@+id/loginFragment2"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.blankfragment.LoginFragment2"
android:label="LoginFragment2" />
Run Code Online (Sandbox Code Playgroud)
ViewPager2的页面导航图之一,其他相同。
nav_graph_home.xml
<fragment
android:id="@+id/home_dest"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_nestednavhost.navhost.HomeNavHostFragment"
android:label="HomeHost"
tools:layout="@layout/fragment_navhost_home" />
<fragment
android:id="@+id/homeFragment1"
android:name="com.smarttoolfactory.tutorial6_2navigationui_viewpager2_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.tutorial6_2navigationui_viewpager2_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.tutorial6_2navigationui_viewpager2_nestednavhost.blankfragment.HomeFragment3"
android:label="HomeFragment3"
tools:layout="@layout/fragment_home3" />
Run Code Online (Sandbox Code Playgroud)
ViewPager 导航图的重要事项是在屏幕上使用片段而不是 NavHost 片段,否则您需要设置导航
if (navController!!.currentDestination == null || navController!!.currentDestination!!.id == navController!!.graph.startDestination) {
navController?.navigate(R.id.homeFragment1)
}
Run Code Online (Sandbox Code Playgroud)
当附加片段的 navHost 时,在 NavHost 片段中。
主要活动
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
listenBackStackChange()
}
private fun listenBackStackChange() {
// 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
Toast.makeText(
this,
"Main graph backStackEntryCount: $backStackEntryCount, fragments: $fragments",
Toast.LENGTH_SHORT
).show()
}
}
}
Run Code Online (Sandbox Code Playgroud)
listenBackStackChange 函数只是观察主片段堆栈和片段如何变化,它只有观察目的,如果不需要就删除它。
ViewPager2 的适配器
class ChildFragmentStateAdapter(private val fragment: Fragment) :
FragmentStateAdapter(fragment) {
override fun getItemCount(): Int = 4
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> HomeNavHostFragment()
1 -> DashBoardNavHostFragment()
2 -> NotificationHostFragment()
else -> LoginFragment1()
}
}
}
Run Code Online (Sandbox Code Playgroud)
带有 HostFragment 的 Fragment 没有应用栏导航,因为它没有在这个例子中实现。
主片段
类 MainFragment : BaseDataBindingFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// TabLayout
val tabLayout = dataBinding.tabLayout
// ViewPager2
val viewPager = dataBinding.viewPager
/*
Set Adapter for ViewPager inside this fragment using this Fragment,
more specifically childFragmentManager as param
*/
viewPager.adapter = ChildFragmentStateAdapter(this)
// Bind tabs and viewpager
TabLayoutMediator(tabLayout, viewPager) { tab, position ->
when(position) {
0-> tab.text = "Home"
1-> tab.text = "Notification"
2-> tab.text = "Dashboard"
3-> tab.text = "Login"
}
}.attach()
}
override fun getLayoutRes(): Int = R.layout.fragment_main
Run Code Online (Sandbox Code Playgroud)
}
MainFragment 设置选项卡,BaseDataBindingFragment仅使用数据绑定通过getLayoutRes()
最后 Pager 的嵌套片段
class HomeNavHostFragment : BaseDataBindingFragment<FragmentNavhostHomeBinding>() {
override fun getLayoutRes(): Int = R.layout.fragment_navhost_home
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)
/*
This is navController we get from findNavController not the one required
for navigating nested fragments
*/
val mainNavController =
Navigation.findNavController(requireActivity(), R.id.nav_host_fragment)
val nestedNavHostFragment =
childFragmentManager.findFragmentById(nestedNavHostFragmentId) as? NavHostFragment
navController = nestedNavHostFragment?.navController
/*
Alternative 1
Navigate to HomeFragment1 if there is no current destination and current destination
is start destination. Set start destination as this fragment so it needs to
navigate next destination.
If start destination is NavHostFragment it's required to navigate to first
*/
// if (navController!!.currentDestination == null || navController!!.currentDestination!!.id == navController!!.graph.startDestination) {
// navController?.navigate(R.id.homeFragment1)
// }
/*
Alternative 2 Reset graph to default status every time this fragment's view is created
? This does not work if initial destination if this fragment because it repeats
creating this fragment in an infinite loop since graph is created every time
*/
// val navInflater = navController!!.navInflater
// nestedNavHostFragment!!.navController.graph = graph
// val graph = navController!!.navInflater.inflate(navGraphId)
// nestedNavHostFragment!!.navController.graph = graph
// Listen on back press
listenOnBackPressed()
}
private fun listenOnBackPressed() {
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, callback)
}
override fun onResume() {
super.onResume()
callback.isEnabled = true
}
override fun onPause() {
super.onPause()
callback.isEnabled = false
}
// This should be false, true causes problems on rotation
val callback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
// Get NavHostFragment
val navHostFragment =
childFragmentManager.findFragmentById(nestedNavHostFragmentId)
// ChildFragmentManager of the current NavHostFragment
val navHostChildFragmentManager = navHostFragment?.childFragmentManager
val currentDestination = navController?.currentDestination
val backStackEntryCount = navHostChildFragmentManager!!.backStackEntryCount
val isAtStartDestination =
(navController?.currentDestination?.id == navController?.graph?.startDestination)
// Check if it's the root of nested fragments in this navhost
if (navController?.currentDestination?.id == navController?.graph?.startDestination) {
/*
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()
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
这里重要的是正确使用 onBackPressedDispatcher 。ViewPager2 中嵌套片段返回导航存在一些问题。
OnBackPressedCallback与 navController?.navigateUp()OnBackPressedCallback在 ViewPager 片段的根目录下使用时HomeFragment1,您无法返回,因为您正在使用navController?.navigateUp(). 要修复它,您应该检查if (navController?.currentDestination?.id == navController?.graph?.startDestination)
是根。requireActivity().onBackPressed()它时handleOnBackPressed,它会创建一个无限循环。所以,之前禁用回调并再次重置它。handleOnBackPressed调用它我创建了其他示例,包括为 ViewPager2 的子片段提供嵌套导航的示例,这是当前项目的链接。对于下图的那个。更棘手的是需要使用 LiveData 并且有旋转问题。还要添加另一个使用 ViewModel 解决此问题的示例。
| 归档时间: |
|
| 查看次数: |
4454 次 |
| 最近记录: |