From 709381c1556552e0280065e14c2de4a8979fe774 Mon Sep 17 00:00:00 2001 From: myung jun Hyun Date: Tue, 26 Nov 2019 17:47:53 +0900 Subject: [PATCH] (#2) Fix Navigation Component logic --- app/build.gradle | 2 + .../java/com/mashup/app/main/MainActivity.kt | 47 +++- .../com/mashup/util/NavigationExtension.kt | 235 ++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 9 +- app/src/main/res/menu/bottom_nav_menu.xml | 4 +- .../{mobile_navigation.xml => notices.xml} | 11 +- app/src/main/res/navigation/setting.xml | 12 + 7 files changed, 298 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/mashup/util/NavigationExtension.kt rename app/src/main/res/navigation/{mobile_navigation.xml => notices.xml} (63%) create mode 100644 app/src/main/res/navigation/setting.xml diff --git a/app/build.gradle b/app/build.gradle index 38de8b7..2517b97 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,6 +37,8 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.1.0' + implementation "androidx.fragment:fragment:1.2.0-rc02" implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.cardview:cardview:1.0.0' implementation "com.google.android.material:material:1.0.0" diff --git a/app/src/main/java/com/mashup/app/main/MainActivity.kt b/app/src/main/java/com/mashup/app/main/MainActivity.kt index d166ff6..03a0d5c 100644 --- a/app/src/main/java/com/mashup/app/main/MainActivity.kt +++ b/app/src/main/java/com/mashup/app/main/MainActivity.kt @@ -2,19 +2,56 @@ package com.mashup.app.main import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.findNavController -import androidx.navigation.ui.setupWithNavController +import androidx.lifecycle.LiveData +import androidx.navigation.NavController import com.google.android.material.bottomnavigation.BottomNavigationView import com.mashup.R +import com.mashup.util.setupWithNavController class MainActivity : AppCompatActivity() { + private var currentNavController: LiveData? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - val navView: BottomNavigationView = findViewById(R.id.nav_view) + 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.nav_view) + + val navGraphIds = listOf(R.navigation.notices, R.navigation.setting) + + // 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 + } - val navController = findNavController(R.id.nav_host_fragment) - navView.setupWithNavController(navController) + override fun onSupportNavigateUp(): Boolean { + return currentNavController?.value?.navigateUp() ?: false } } diff --git a/app/src/main/java/com/mashup/util/NavigationExtension.kt b/app/src/main/java/com/mashup/util/NavigationExtension.kt new file mode 100644 index 0000000..29056ac --- /dev/null +++ b/app/src/main/java/com/mashup/util/NavigationExtension.kt @@ -0,0 +1,235 @@ +package com.mashup.util + +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.google.android.material.bottomnavigation.BottomNavigationView +import com.mashup.R + +/** + * 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" diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index fc0f7a8..cffad19 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -17,17 +17,14 @@ app:layout_constraintRight_toRightOf="parent" app:menu="@menu/bottom_nav_menu" /> - + app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/notices.xml similarity index 63% rename from app/src/main/res/navigation/mobile_navigation.xml rename to app/src/main/res/navigation/notices.xml index 70890cc..1da91ff 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/notices.xml @@ -2,18 +2,11 @@ - - - - \ No newline at end of file + diff --git a/app/src/main/res/navigation/setting.xml b/app/src/main/res/navigation/setting.xml new file mode 100644 index 0000000..8d04f28 --- /dev/null +++ b/app/src/main/res/navigation/setting.xml @@ -0,0 +1,12 @@ + + + +