NavigationExtensions.kt 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package io.r_a_d.radio2
  2. import android.content.Intent
  3. import android.util.SparseArray
  4. import androidx.core.util.forEach
  5. import androidx.core.util.set
  6. import androidx.fragment.app.FragmentManager
  7. import androidx.lifecycle.LiveData
  8. import androidx.lifecycle.MutableLiveData
  9. import androidx.navigation.NavController
  10. import androidx.navigation.fragment.NavHostFragment
  11. import com.google.android.material.bottomnavigation.BottomNavigationView
  12. /**
  13. * YATTOZ' NOTE : this file has been scavenged from Android's "architecture-components-samples" repo.
  14. * See it here:
  15. * https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt
  16. * it allows, among other things, to keep and restore fragments' state when they're swapped. Useful for IRC.
  17. */
  18. /**
  19. * Manages the various graphs needed for a [BottomNavigationView].
  20. *
  21. * This sample is a workaround until the Navigation Component supports multiple back stacks.
  22. */
  23. fun BottomNavigationView.setupWithNavController(
  24. navGraphIds: List<Int>,
  25. fragmentManager: FragmentManager,
  26. containerId: Int,
  27. intent: Intent
  28. ): LiveData<NavController> {
  29. // Map of tags
  30. val graphIdToTagMap = SparseArray<String>()
  31. // Result. Mutable live data with the selected controlled
  32. val selectedNavController = MutableLiveData<NavController>()
  33. var firstFragmentGraphId = 0
  34. // First create a NavHostFragment for each NavGraph ID
  35. navGraphIds.forEachIndexed { index, navGraphId ->
  36. val fragmentTag = getFragmentTag(index)
  37. // Find or create the Navigation host fragment
  38. val navHostFragment = obtainNavHostFragment(
  39. fragmentManager,
  40. fragmentTag,
  41. navGraphId,
  42. containerId
  43. )
  44. // Obtain its id
  45. val graphId = navHostFragment.navController.graph.id
  46. if (index == 0) {
  47. firstFragmentGraphId = graphId
  48. }
  49. // Save to the map
  50. graphIdToTagMap[graphId] = fragmentTag
  51. // Attach or detach nav host fragment depending on whether it's the selected item.
  52. if (this.selectedItemId == graphId) {
  53. // Update livedata with the selected graph
  54. selectedNavController.value = navHostFragment.navController
  55. attachNavHostFragment(fragmentManager, navHostFragment, index == 0)
  56. } else {
  57. detachNavHostFragment(fragmentManager, navHostFragment)
  58. }
  59. }
  60. // Now connect selecting an item with swapping Fragments
  61. var selectedItemTag = graphIdToTagMap[this.selectedItemId]
  62. val firstFragmentTag = graphIdToTagMap[firstFragmentGraphId]
  63. var isOnFirstFragment = selectedItemTag == firstFragmentTag
  64. // When a navigation item is selected
  65. setOnNavigationItemSelectedListener { item ->
  66. // Don't do anything if the state is state has already been saved.
  67. if (fragmentManager.isStateSaved) {
  68. false
  69. } else {
  70. val newlySelectedItemTag = graphIdToTagMap[item.itemId]
  71. if (selectedItemTag != newlySelectedItemTag) {
  72. // Pop everything above the first fragment (the "fixed start destination")
  73. fragmentManager.popBackStack(firstFragmentTag,
  74. FragmentManager.POP_BACK_STACK_INCLUSIVE)
  75. val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
  76. as NavHostFragment
  77. // Exclude the first fragment tag because it's always in the back stack.
  78. if (firstFragmentTag != newlySelectedItemTag) {
  79. // Commit a transaction that cleans the back stack and adds the first fragment
  80. // to it, creating the fixed started destination.
  81. fragmentManager.beginTransaction()
  82. /* // YATTOZ' NOTE - disabling animations, it feels snappier and more in place.
  83. .setCustomAnimations(
  84. R.anim.nav_default_enter_anim,
  85. R.anim.nav_default_exit_anim,
  86. R.anim.nav_default_pop_enter_anim,
  87. R.anim.nav_default_pop_exit_anim)
  88. */
  89. .attach(selectedFragment)
  90. .setPrimaryNavigationFragment(selectedFragment)
  91. .apply {
  92. // Detach all other Fragments
  93. graphIdToTagMap.forEach { _, fragmentTagIter ->
  94. if (fragmentTagIter != newlySelectedItemTag) {
  95. detach(fragmentManager.findFragmentByTag(firstFragmentTag)!!)
  96. }
  97. }
  98. }
  99. .addToBackStack(firstFragmentTag)
  100. .setReorderingAllowed(true)
  101. .commit()
  102. }
  103. selectedItemTag = newlySelectedItemTag
  104. isOnFirstFragment = selectedItemTag == firstFragmentTag
  105. selectedNavController.value = selectedFragment.navController
  106. true
  107. } else {
  108. false
  109. }
  110. }
  111. }
  112. // Optional: on item reselected, pop back stack to the destination of the graph
  113. setupItemReselected(graphIdToTagMap, fragmentManager)
  114. // Handle deep link
  115. setupDeepLinks(navGraphIds, fragmentManager, containerId, intent)
  116. // Finally, ensure that we update our BottomNavigationView when the back stack changes
  117. fragmentManager.addOnBackStackChangedListener {
  118. if (!isOnFirstFragment && !fragmentManager.isOnBackStack(firstFragmentTag)) {
  119. this.selectedItemId = firstFragmentGraphId
  120. }
  121. // Reset the graph if the currentDestination is not valid (happens when the back
  122. // stack is popped after using the back button).
  123. selectedNavController.value?.let { controller ->
  124. if (controller.currentDestination == null) {
  125. controller.navigate(controller.graph.id)
  126. }
  127. }
  128. }
  129. return selectedNavController
  130. }
  131. private fun BottomNavigationView.setupDeepLinks(
  132. navGraphIds: List<Int>,
  133. fragmentManager: FragmentManager,
  134. containerId: Int,
  135. intent: Intent
  136. ) {
  137. navGraphIds.forEachIndexed { index, navGraphId ->
  138. val fragmentTag = getFragmentTag(index)
  139. // Find or create the Navigation host fragment
  140. val navHostFragment = obtainNavHostFragment(
  141. fragmentManager,
  142. fragmentTag,
  143. navGraphId,
  144. containerId
  145. )
  146. // Handle Intent
  147. if (navHostFragment.navController.handleDeepLink(intent)
  148. && selectedItemId != navHostFragment.navController.graph.id) {
  149. this.selectedItemId = navHostFragment.navController.graph.id
  150. }
  151. }
  152. }
  153. private fun BottomNavigationView.setupItemReselected(
  154. graphIdToTagMap: SparseArray<String>,
  155. fragmentManager: FragmentManager
  156. ) {
  157. setOnNavigationItemReselectedListener { item ->
  158. val newlySelectedItemTag = graphIdToTagMap[item.itemId]
  159. val selectedFragment = fragmentManager.findFragmentByTag(newlySelectedItemTag)
  160. as NavHostFragment
  161. val navController = selectedFragment.navController
  162. // Pop the back stack to the start destination of the current navController graph
  163. navController.popBackStack(
  164. navController.graph.startDestination, false
  165. )
  166. }
  167. }
  168. private fun detachNavHostFragment(
  169. fragmentManager: FragmentManager,
  170. navHostFragment: NavHostFragment
  171. ) {
  172. fragmentManager.beginTransaction()
  173. .detach(navHostFragment)
  174. .commitNow()
  175. }
  176. private fun attachNavHostFragment(
  177. fragmentManager: FragmentManager,
  178. navHostFragment: NavHostFragment,
  179. isPrimaryNavFragment: Boolean
  180. ) {
  181. fragmentManager.beginTransaction()
  182. .attach(navHostFragment)
  183. .apply {
  184. if (isPrimaryNavFragment) {
  185. setPrimaryNavigationFragment(navHostFragment)
  186. }
  187. }
  188. .commitNow()
  189. }
  190. private fun obtainNavHostFragment(
  191. fragmentManager: FragmentManager,
  192. fragmentTag: String,
  193. navGraphId: Int,
  194. containerId: Int
  195. ): NavHostFragment {
  196. // If the Nav Host fragment exists, return it
  197. val existingFragment = fragmentManager.findFragmentByTag(fragmentTag) as NavHostFragment?
  198. existingFragment?.let { return it }
  199. // Otherwise, create it and return it.
  200. val navHostFragment = NavHostFragment.create(navGraphId)
  201. fragmentManager.beginTransaction()
  202. .add(containerId, navHostFragment, fragmentTag)
  203. .commitNow()
  204. return navHostFragment
  205. }
  206. private fun FragmentManager.isOnBackStack(backStackName: String): Boolean {
  207. val backStackCount = backStackEntryCount
  208. for (index in 0 until backStackCount) {
  209. if (getBackStackEntryAt(index).name == backStackName) {
  210. return true
  211. }
  212. }
  213. return false
  214. }
  215. private fun getFragmentTag(index: Int) = "bottomNavigation#$index"