diff --git a/app/src/main/java/com/aaglobal/jnc_playground/RootActivity.kt b/app/src/main/java/com/aaglobal/jnc_playground/RootActivity.kt index a0702cc..b61508b 100644 --- a/app/src/main/java/com/aaglobal/jnc_playground/RootActivity.kt +++ b/app/src/main/java/com/aaglobal/jnc_playground/RootActivity.kt @@ -1,26 +1,59 @@ package com.aaglobal.jnc_playground import android.os.Bundle -import com.google.android.material.bottomnavigation.BottomNavigationView import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.findNavController -import androidx.navigation.ui.AppBarConfiguration +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.navigation.NavController import androidx.navigation.ui.setupActionBarWithNavController -import androidx.navigation.ui.setupWithNavController +import com.aaglobal.jnc_playground.extensions.setupWithNavController +import com.google.android.material.bottomnavigation.BottomNavigationView class RootActivity : AppCompatActivity() { + private var currentNavController: LiveData? = null + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_root) - val navView: BottomNavigationView = findViewById(R.id.nav_view) - - val navController = findNavController(R.id.nav_host_fragment) - // Passing each menu ID as a set of Ids because each - // menu should be considered as top level destinations. - val appBarConfiguration = AppBarConfiguration(setOf( - R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)) - setupActionBarWithNavController(navController, appBarConfiguration) - navView.setupWithNavController(navController) + if (savedInstanceState == null) { + setupBottomNavigationBar() + } // Else, need to wait for onRestoreInstanceState + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + // Now that BottomNavigationBar has restored its instance state + // and its selectedItemId, we can proceed with setting up the + // BottomNavigationBar with Navigation + setupBottomNavigationBar() + } + + /** + * Called on first creation and when restoring state. + */ + private fun setupBottomNavigationBar() { + val bottomNavigationView = findViewById(R.id.bottom_nav) + + val navGraphIds = listOf(R.navigation.home_nav_graph, R.navigation.dashboard_nav_graph, R.navigation.notifications_nav_graph) + + // Setup the bottom navigation view with a list of navigation graphs + val controller = bottomNavigationView.setupWithNavController( + navGraphIds = navGraphIds, + fragmentManager = supportFragmentManager, + containerId = R.id.nav_host_container, + intent = intent + ) + + // Whenever the selected controller changes, setup the action bar. + controller.observe(this, Observer { navController -> + setupActionBarWithNavController(navController) + }) + currentNavController = controller + } + + override fun onSupportNavigateUp(): Boolean { + return currentNavController?.value?.navigateUp() ?: false } } \ No newline at end of file diff --git a/app/src/main/java/com/aaglobal/jnc_playground/extensions/NavigationExtensions.kt b/app/src/main/java/com/aaglobal/jnc_playground/extensions/NavigationExtensions.kt new file mode 100644 index 0000000..95216c7 --- /dev/null +++ b/app/src/main/java/com/aaglobal/jnc_playground/extensions/NavigationExtensions.kt @@ -0,0 +1,235 @@ +package com.aaglobal.jnc_playground.extensions + +import android.content.Intent +import android.util.SparseArray +import androidx.core.util.forEach +import androidx.core.util.set +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import com.aaglobal.jnc_playground.R +import com.google.android.material.bottomnavigation.BottomNavigationView + +/** + * Manages the various graphs needed for a [BottomNavigationView]. + * + * This sample is a workaround until the Navigation Component supports multiple back stacks. + */ +fun BottomNavigationView.setupWithNavController( + navGraphIds: List, + fragmentManager: FragmentManager, + containerId: Int, + intent: Intent +): LiveData { + + // Map of tags + val graphIdToTagMap = SparseArray() + // Result. Mutable live data with the selected controlled + val selectedNavController = MutableLiveData() + + var firstFragmentGraphId = 0 + + // First create a NavHostFragment for each NavGraph ID + navGraphIds.forEachIndexed { index, navGraphId -> + val fragmentTag = getFragmentTag(index) + + // Find or create the Navigation host fragment + val navHostFragment = obtainNavHostFragment( + fragmentManager, + fragmentTag, + navGraphId, + containerId + ) + + // Obtain its id + val graphId = navHostFragment.navController.graph.id + + if (index == 0) { + firstFragmentGraphId = graphId + } + + // Save to the map + graphIdToTagMap[graphId] = fragmentTag + + // Attach or detach nav host fragment depending on whether it's the selected item. + if (this.selectedItemId == graphId) { + // Update livedata with the selected graph + selectedNavController.value = navHostFragment.navController + attachNavHostFragment(fragmentManager, navHostFragment, index == 0) + } else { + detachNavHostFragment(fragmentManager, navHostFragment) + } + } + + // Now connect selecting an item with swapping Fragments + var selectedItemTag = graphIdToTagMap[this.selectedItemId] + val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId] + var isOnFirstFragment = selectedItemTag == firstFragmentTag + + // When a navigation item is selected + setOnNavigationItemSelectedListener { item -> + // Don't do anything if the state is state has already been saved. + if (fragmentManager.isStateSaved) { + false + } else { + val newlySelectedItemTag = graphIdToTagMap[item.itemId] + if (selectedItemTag != newlySelectedItemTag) { + // Pop everything above the first fragment (the "fixed start destination") + fragmentManager.popBackStack(firstFragmentTag, + FragmentManager.POP_BACK_STACK_INCLUSIVE) + val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag) + as NavHostFragment + + // Exclude the first fragment tag because it's always in the back stack. + if (firstFragmentTag != newlySelectedItemTag) { + // Commit a transaction that cleans the back stack and adds the first fragment + // to it, creating the fixed started destination. + fragmentManager.beginTransaction() + .setCustomAnimations( + R.anim.nav_default_enter_anim, + R.anim.nav_default_exit_anim, + R.anim.nav_default_pop_enter_anim, + R.anim.nav_default_pop_exit_anim) + .attach(selectedFragment) + .setPrimaryNavigationFragment(selectedFragment) + .apply { + // Detach all other Fragments + graphIdToTagMap.forEach { _, fragmentTagIter -> + if (fragmentTagIter != newlySelectedItemTag) { + detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!) + } + } + } + .addToBackStack(firstFragmentTag) + .setReorderingAllowed(true) + .commit() + } + selectedItemTag = newlySelectedItemTag + isOnFirstFragment = selectedItemTag == firstFragmentTag + selectedNavController.value = selectedFragment.navController + true + } else { + false + } + } + } + + // Optional: on item reselected, pop back stack to the destination of the graph + setupItemReselected(graphIdToTagMap, fragmentManager) + + // Handle deep link + setupDeepLinks(navGraphIds, fragmentManager, containerId, intent) + + // Finally, ensure that we update our BottomNavigationView when the back stack changes + fragmentManager.addOnBackStackChangedListener { + if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) { + this.selectedItemId = firstFragmentGraphId + } + + // Reset the graph if the currentDestination is not valid (happens when the back + // stack is popped after using the back button). + selectedNavController.value?.let { controller -> + if (controller.currentDestination == null) { + controller.navigate(controller.graph.id) + } + } + } + return selectedNavController +} + +private fun BottomNavigationView.setupDeepLinks( + navGraphIds: List, + fragmentManager: FragmentManager, + containerId: Int, + intent: Intent +) { + navGraphIds.forEachIndexed { index, navGraphId -> + val fragmentTag = getFragmentTag(index) + + // Find or create the Navigation host fragment + val navHostFragment = obtainNavHostFragment( + fragmentManager, + fragmentTag, + navGraphId, + containerId + ) + // Handle Intent + if (navHostFragment.navController.handleDeepLink(intent) + && selectedItemId != navHostFragment.navController.graph.id) { + this.selectedItemId = navHostFragment.navController.graph.id + } + } +} + +private fun BottomNavigationView.setupItemReselected( + graphIdToTagMap: SparseArray, + fragmentManager: FragmentManager +) { + setOnNavigationItemReselectedListener { item -> + val newlySelectedItemTag = graphIdToTagMap[item.itemId] + val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag) + as NavHostFragment + val navController = selectedFragment.navController + // Pop the back stack to the start destination of the current navController graph + navController.popBackStack( + navController.graph.startDestination, false + ) + } +} + +private fun detachNavHostFragment( + fragmentManager: FragmentManager, + navHostFragment: NavHostFragment +) { + fragmentManager.beginTransaction() + .detach(navHostFragment) + .commitNow() +} + +private fun attachNavHostFragment( + fragmentManager: FragmentManager, + navHostFragment: NavHostFragment, + isPrimaryNavFragment: Boolean +) { + fragmentManager.beginTransaction() + .attach(navHostFragment) + .apply { + if (isPrimaryNavFragment) { + setPrimaryNavigationFragment(navHostFragment) + } + } + .commitNow() + +} + +private fun obtainNavHostFragment( + fragmentManager: FragmentManager, + fragmentTag: String, + navGraphId: Int, + containerId: Int +): NavHostFragment { + // If the Nav Host fragment exists, return it + val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment? + existingFragment?.let { return it } + + // Otherwise, create it and return it. + val navHostFragment = NavHostFragment.create(navGraphId) + fragmentManager.beginTransaction() + .add(containerId, navHostFragment, fragmentTag) + .commitNow() + return navHostFragment +} + +private fun FragmentManager.isOnBackStack(backStackName: String): Boolean { + val backStackCount = backStackEntryCount + for (index in 0 until backStackCount) { + if (getBackStackEntryAt(index).name == backStackName) { + return true + } + } + return false +} + +private fun getFragmentTag(index: Int) = "bottomNavigation#$index" \ No newline at end of file diff --git a/app/src/main/res/layout/activity_root.xml b/app/src/main/res/layout/activity_root.xml index 38c0aa8..29e5904 100644 --- a/app/src/main/res/layout/activity_root.xml +++ b/app/src/main/res/layout/activity_root.xml @@ -1,33 +1,20 @@ - + android:orientation="vertical"> + + - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/dashboard_nav_graph.xml similarity index 55% rename from app/src/main/res/navigation/mobile_navigation.xml rename to app/src/main/res/navigation/dashboard_nav_graph.xml index b18583e..d659630 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/dashboard_nav_graph.xml @@ -2,17 +2,11 @@ + android:id="@+id/navigation_dashboard" + app:startDestination="@id/DashboardFragment"> - - @@ -23,12 +17,6 @@ - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/notifications_nav_graph.xml b/app/src/main/res/navigation/notifications_nav_graph.xml new file mode 100644 index 0000000..94222af --- /dev/null +++ b/app/src/main/res/navigation/notifications_nav_graph.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file