Android 选择底部导航选项卡后清除返回堆栈
Android clear backstack after reselecting Bottom Navigation tab
使用最新的 Navigation Component 和 BottomNavigationView
,NavController
现在默认保存和恢复选项卡的状态:
As part of this change, the NavigationUI methods of onNavDestinationSelected(), BottomNavigationView.setupWithNavController() and NavigationView.setupWithNavController() now automatically save and restore the state of popped destinations, enabling support for multiple back stacks without any code changes. When using Navigation with Fragments, this is the recommended way to integrate with multiple back stacks.
太棒了!现在切换选项卡会为您提供上次查看的堆栈。
但是,如果用户 reselects 一个标签,说他们已经离开 Home -> Detail Page A -> Detail Page B
,那么他们 select Home
选项卡希望返回默认视图,他们仍然看到 Detail Page B
。
似乎部分讨论是处理 issue tracker 中提到的“重新select 选项卡”行为,但我想不出推荐的实施方式这个。
NavigationAdvancedSample 中包含的所有内容是:
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNavigationView.setupWithNavController(navController)
// Setup the ActionBar with navController and 3 top level destinations
appBarConfiguration = AppBarConfiguration(
setOf(R.id.titleScreen, R.id.leaderboard, R.id.register)
)
setupActionBarWithNavController(navController, appBarConfiguration)
这只是恢复以前的状态,如发行说明中所述。
我们如何检查第二次点击导航栏项目并清除返回堆栈?
BottomNavigationView
有自己的方法通过 setOnItemReselectedListener()
(or, when using an earlier version of the Material Design Library, the now deprecated setOnNavigationItemReselectedListener()
).
处理重选
bottomNavigationView.setupWithNavController
没有设置这个监听器(因为没有 Material 规范重新选择标签应该做什么),所以你需要自己设置它:
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNavigationView.setupWithNavController(navController)
// Add your own reselected listener
bottomNavigationView.setOnItemReselectedListener { item ->
// Pop everything up to the reselected item
val reselectedDestinationId = item.itemId
navController.popBackStack(reselectedDestinationId, inclusive = false)
}
使用您自己的 setupWithNavController2
而不是 androidx.navigation.ui.BottomNavigationViewKt
中的 setupWithNavController
例如:
在导航之前添加了对已选择项目的检查:
if (navController.popBackStack(item.itemId, false)) {
return true
}
查看 onNavDestinationSelected
的评论,setupWithNavController2
的完整代码:
fun BottomNavigationView.setupWithNavController2(navController: NavController) {
val bottomNavigationView = this
bottomNavigationView.setOnItemSelectedListener { item ->
onNavDestinationSelected(item, navController)
}
val weakReference = WeakReference(bottomNavigationView)
navController.addOnDestinationChangedListener(
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
val view = weakReference.get()
if (view == null) {
navController.removeOnDestinationChangedListener(this)
return
}
val menu = view.menu
var i = 0
val size = menu.size()
while (i < size) {
val item = menu.getItem(i)
if (matchDestination(destination, item.itemId)) {
item.isChecked = true
}
i++
}
}
})
// Add your own reselected listener
bottomNavigationView.setOnItemReselectedListener { item ->
// Pop everything up to the reselected item
val reselectedDestinationId = item.itemId
navController.popBackStack(reselectedDestinationId, false)
}
}
fun onNavDestinationSelected(
item: MenuItem,
navController: NavController
): Boolean {
val builder = NavOptions.Builder()
.setLaunchSingleTop(true)
if (navController.currentDestination?.parent?.findNode(item.itemId) is ActivityNavigator.Destination) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(R.animator.nav_default_enter_anim)
.setExitAnim(R.animator.nav_default_exit_anim)
.setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
.setPopExitAnim(R.animator.nav_default_pop_exit_anim)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
val findStartDestination = findStartDestination(navController.graph)
if (findStartDestination != null) {
builder.setPopUpTo(findStartDestination.id, false)
}
}
val options = builder.build()
//region The code was added to avoid adding already exist item
if (navController.popBackStack(item.itemId, false)) {
return true
}
//endregion
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, null, options)
true
} catch (e: IllegalArgumentException) {
false
}
}
fun findStartDestination(graph: NavGraph): NavDestination? {
var startDestination: NavDestination? = graph
while (startDestination is NavGraph) {
val parent = startDestination
startDestination = parent.findNode(parent.startDestination)
}
return startDestination
}
fun matchDestination(
destination: NavDestination,
@IdRes destId: Int
): Boolean {
var currentDestination: NavDestination? = destination
while (currentDestination?.id != destId && currentDestination?.parent != null) {
currentDestination = currentDestination.parent
}
return currentDestination?.id == destId
}
接受的答案让我开始了正确的方向。然而,随着 Android 导航库 2.4.0 中支持的多个后台堆栈的添加,这就是最终对我有用的东西:
val currentRootFragment = supportFragmentManager.findFragmentById(R.id.main_fragment)
val navHost = currentRootFragment as? NavHostFragment
val selectedMenuItemNavGraph = navHost?.navController?.graph?.findNode(menuItem.itemId) as? NavGraph?
selectedMenuItemNavGraph?.let { menuGraph ->
navHost?.navController?.popBackStack(menuGraph.startDestinationId, false)
}
这里有两种方法可以解决问题...
在项目 selected 之后更新 selection(带有返回堆栈的项目,最新版本 - 2.4.2,当存在返回堆栈时在最上面的目的地,select该项目不会 select 该项目首先)。
NavigationBarView.setOnItemSelectedListener {}
等待第二次点击,执行返回栈的出栈。
NavigationBarView.setOnItemReselectedListener {}
最终代码是,
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
val navController = navHostFragment?.navController
mainBinding?.bottomNavigation?.apply {
navController?.let { navController ->
NavigationUI.setupWithNavController(
this,
navController
)
setOnItemSelectedListener { item ->
NavigationUI.onNavDestinationSelected(item, navController)
true
}
setOnItemReselectedListener {
navController.popBackStack(destinationId = it.itemId, inclusive = false)
}
}
}
希望这会有所帮助..
使用最新的 Navigation Component 和 BottomNavigationView
,NavController
现在默认保存和恢复选项卡的状态:
As part of this change, the NavigationUI methods of onNavDestinationSelected(), BottomNavigationView.setupWithNavController() and NavigationView.setupWithNavController() now automatically save and restore the state of popped destinations, enabling support for multiple back stacks without any code changes. When using Navigation with Fragments, this is the recommended way to integrate with multiple back stacks.
太棒了!现在切换选项卡会为您提供上次查看的堆栈。
但是,如果用户 reselects 一个标签,说他们已经离开 Home -> Detail Page A -> Detail Page B
,那么他们 select Home
选项卡希望返回默认视图,他们仍然看到 Detail Page B
。
似乎部分讨论是处理 issue tracker 中提到的“重新select 选项卡”行为,但我想不出推荐的实施方式这个。
NavigationAdvancedSample 中包含的所有内容是:
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNavigationView.setupWithNavController(navController)
// Setup the ActionBar with navController and 3 top level destinations
appBarConfiguration = AppBarConfiguration(
setOf(R.id.titleScreen, R.id.leaderboard, R.id.register)
)
setupActionBarWithNavController(navController, appBarConfiguration)
这只是恢复以前的状态,如发行说明中所述。
我们如何检查第二次点击导航栏项目并清除返回堆栈?
BottomNavigationView
有自己的方法通过 setOnItemReselectedListener()
(or, when using an earlier version of the Material Design Library, the now deprecated setOnNavigationItemReselectedListener()
).
bottomNavigationView.setupWithNavController
没有设置这个监听器(因为没有 Material 规范重新选择标签应该做什么),所以你需要自己设置它:
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
bottomNavigationView.setupWithNavController(navController)
// Add your own reselected listener
bottomNavigationView.setOnItemReselectedListener { item ->
// Pop everything up to the reselected item
val reselectedDestinationId = item.itemId
navController.popBackStack(reselectedDestinationId, inclusive = false)
}
使用您自己的 setupWithNavController2
而不是 androidx.navigation.ui.BottomNavigationViewKt
setupWithNavController
例如:
在导航之前添加了对已选择项目的检查:
if (navController.popBackStack(item.itemId, false)) {
return true
}
查看 onNavDestinationSelected
的评论,setupWithNavController2
的完整代码:
fun BottomNavigationView.setupWithNavController2(navController: NavController) {
val bottomNavigationView = this
bottomNavigationView.setOnItemSelectedListener { item ->
onNavDestinationSelected(item, navController)
}
val weakReference = WeakReference(bottomNavigationView)
navController.addOnDestinationChangedListener(
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
val view = weakReference.get()
if (view == null) {
navController.removeOnDestinationChangedListener(this)
return
}
val menu = view.menu
var i = 0
val size = menu.size()
while (i < size) {
val item = menu.getItem(i)
if (matchDestination(destination, item.itemId)) {
item.isChecked = true
}
i++
}
}
})
// Add your own reselected listener
bottomNavigationView.setOnItemReselectedListener { item ->
// Pop everything up to the reselected item
val reselectedDestinationId = item.itemId
navController.popBackStack(reselectedDestinationId, false)
}
}
fun onNavDestinationSelected(
item: MenuItem,
navController: NavController
): Boolean {
val builder = NavOptions.Builder()
.setLaunchSingleTop(true)
if (navController.currentDestination?.parent?.findNode(item.itemId) is ActivityNavigator.Destination) {
builder.setEnterAnim(R.anim.nav_default_enter_anim)
.setExitAnim(R.anim.nav_default_exit_anim)
.setPopEnterAnim(R.anim.nav_default_pop_enter_anim)
.setPopExitAnim(R.anim.nav_default_pop_exit_anim)
} else {
builder.setEnterAnim(R.animator.nav_default_enter_anim)
.setExitAnim(R.animator.nav_default_exit_anim)
.setPopEnterAnim(R.animator.nav_default_pop_enter_anim)
.setPopExitAnim(R.animator.nav_default_pop_exit_anim)
}
if (item.order and Menu.CATEGORY_SECONDARY == 0) {
val findStartDestination = findStartDestination(navController.graph)
if (findStartDestination != null) {
builder.setPopUpTo(findStartDestination.id, false)
}
}
val options = builder.build()
//region The code was added to avoid adding already exist item
if (navController.popBackStack(item.itemId, false)) {
return true
}
//endregion
return try {
// TODO provide proper API instead of using Exceptions as Control-Flow.
navController.navigate(item.itemId, null, options)
true
} catch (e: IllegalArgumentException) {
false
}
}
fun findStartDestination(graph: NavGraph): NavDestination? {
var startDestination: NavDestination? = graph
while (startDestination is NavGraph) {
val parent = startDestination
startDestination = parent.findNode(parent.startDestination)
}
return startDestination
}
fun matchDestination(
destination: NavDestination,
@IdRes destId: Int
): Boolean {
var currentDestination: NavDestination? = destination
while (currentDestination?.id != destId && currentDestination?.parent != null) {
currentDestination = currentDestination.parent
}
return currentDestination?.id == destId
}
接受的答案让我开始了正确的方向。然而,随着 Android 导航库 2.4.0 中支持的多个后台堆栈的添加,这就是最终对我有用的东西:
val currentRootFragment = supportFragmentManager.findFragmentById(R.id.main_fragment)
val navHost = currentRootFragment as? NavHostFragment
val selectedMenuItemNavGraph = navHost?.navController?.graph?.findNode(menuItem.itemId) as? NavGraph?
selectedMenuItemNavGraph?.let { menuGraph ->
navHost?.navController?.popBackStack(menuGraph.startDestinationId, false)
}
这里有两种方法可以解决问题...
在项目 selected 之后更新 selection(带有返回堆栈的项目,最新版本 - 2.4.2,当存在返回堆栈时在最上面的目的地,select该项目不会 select 该项目首先)。
NavigationBarView.setOnItemSelectedListener {}
等待第二次点击,执行返回栈的出栈。
NavigationBarView.setOnItemReselectedListener {}
最终代码是,
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment?
val navController = navHostFragment?.navController
mainBinding?.bottomNavigation?.apply {
navController?.let { navController ->
NavigationUI.setupWithNavController(
this,
navController
)
setOnItemSelectedListener { item ->
NavigationUI.onNavDestinationSelected(item, navController)
true
}
setOnItemReselectedListener {
navController.popBackStack(destinationId = it.itemId, inclusive = false)
}
}
}
希望这会有所帮助..