简体   繁体   中英

Android clear backstack after reselecting Bottom Navigation tab

Using the newest Navigation Component with the BottomNavigationView , the NavController now saves and restores the states of tabs by default:

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.

This is great. Now switching tabs gives you the last viewed stack.

But, if the user reselects a tab, say they've gone Home -> Detail Page A -> Detail Page B , then they select the Home tab expecting to go back to the default view, they still see Detail Page B .

It seems like part of the discussion was to handle the "reselecting a tab" behavior as mentioned in the issue tracker , but I can't figure out the recommended way for implementing this.

All that's included in the NavigationAdvancedSample is:

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)

And this just restores the previous state, as noted in the release notes.

How can we check for tapping a nav bar item a second time and clear the back stack?

BottomNavigationView has its own method for handling reselection via setOnItemReselectedListener() (or, when using an earlier version of the Material Design Library, the now deprecated setOnNavigationItemReselectedListener() ).

bottomNavigationView.setupWithNavController does not set this listener (as there is no Material specification for exactly what reselecting a tab should do), so you need to set it yourself:

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)
}

The accepted answer got me started in the right direction. However, with the addition of multiple backstacks supported in Android navigation library 2.4.0, this is what ended up working for me:

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)
}

Use your own setupWithNavController2 instead of setupWithNavController from androidx.navigation.ui.BottomNavigationViewKt

For ex.:

Added check of already selected item before to navigate :

   if (navController.popBackStack(item.itemId, false)) {
        return true
    }

See comments at onNavDestinationSelected , full code of 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
}

Two methods come to the rescue here...

  1. To update the selection after the item selected (item with back stack, with the latest version - 2.4.2, when there is a back stack present in top destination, selecting that item won't select the item first).

    NavigationBarView.setOnItemSelectedListener {}

  2. To wait for the second click and perform popping of the back stack.

    NavigationBarView.setOnItemReselectedListener {}

Final code is ,

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)
            }
        }

}

Hope this will help..

you have to show bottom navigation bar only for R.id.titleScreen, R.id.leaderboard, R.id.register fragments, for others it should be hidden and they should have just the toolbar with back button

This solution is made from Callie's and ianhanniballake's solution.

The behaviour is

  1. Home -> Detail Page A -> Detail Page B then press Home button -> Home

  2. Home -> Detail Page A -> Detail Page B then navigate to Profile then back to Home will show Detail Page B then Home pressed again will trigger 1 behaviour (so will go back to the Home screen.

     // navView = id of BottomNavigationView navController = findNavController(R.id.nav_host_container) // where nav_host_container is the id of the fragment which is your default Nav host. binding.navView.setOnItemReselectedListener { menuItem -> val selectedMenuItemNavGraph = navController.graph.findNode(menuItem.itemId) as NavGraph selectedMenuItemNavGraph.let { menuGraph -> navController.popBackStack(menuGraph.startDestinationId, false) } }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM