Skip to content

Instantly share code, notes, and snippets.

@StefMa
Last active October 22, 2020 16:16
Show Gist options
  • Save StefMa/5a6d99a8948f0a1b80cfbf5bd4b51c20 to your computer and use it in GitHub Desktop.
Save StefMa/5a6d99a8948f0a1b80cfbf5bd4b51c20 to your computer and use it in GitHub Desktop.
BottomNavigation with Conductor

BottomNavigation with Conductor

Just a simple example to handle the material BottomNavigation with Conductor.

More

This behaviour don't follow the material specs! The guidline says:

Navigation through the bottom navigation bar should reset the task.

Which isn't applied here. This example behaves "exactly" like the iOS TabBar.

Example

Credits

Credits are going to chris6647 which came up with that idea in this PR.

<?xml version="1.0" encoding="utf-8"?>
<com.bluelinelabs.conductor.ChangeHandlerFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/changeHandlerFrameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.bluelinelabs.conductor.ChangeHandlerFrameLayout
android:id="@+id/home_changeHandlerFrameLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<android.support.design.widget.BottomNavigationView
android:id="@+id/navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="0dp"
android:layout_marginStart="0dp"
android:background="@color/navigation_bg"
app:itemIconTint="@android:color/white"
app:itemTextColor="@android:color/white"
app:menu="@menu/navigation" />
</LinearLayout>
private const val ROUTER_STATES_KEY = "STATE"
/**
* This is the base implementation of a [Controller] which works hand in hand with the [BottomNavigationView].
*
* It is designed to work like that:
* * [Textual explanation](https://i.imgur.com/EqqQyOY.png)
* * [Visual explanation](https://i.imgur.com/FDb6EGU.png)
*
* In other words. It should be behave exactly like the [iOS TabBar](http://apple.co/2y6XIrL)
*
* **How does it work?**
*
* If one item in the [BottomNavigationView] we do three things:
* * Save the current [Router.saveInstanceState] in the [routerStates] with the [BottomNavigationView.getSelectedItemId] as key. See [saveStateFromCurrentTab]
* * Clear the current [Router] hierachy and backstack and everything (cleanup). See [clearStateFromChildRouter]
* * Try to restore the [Router.restoreInstanceState] with the saved state contains in [routerStates]. See [tryToRestoreStateFromNewTab] and [onNavigationItemSelected]
*
* The main idea came from [this PR](https://github.com/bluelinelabs/Conductor/pull/316).
*/
class HomeController : Controller(), BottomNavigationView.OnNavigationItemSelectedListener {
/**
* This will hold all the information about the tabs.
*
* This needs to be a var because we have to reassign it in [onRestoreInstanceState]
*/
private var routerStates = SparseArray<Bundle>()
private lateinit var childRouter: Router
/**
* This is the current selected item id from the [BottomNavigationView]
*/
@IdRes
private var currentSelectedItemId: Int = -1
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
val view = inflater.inflate(R.layout.controller_home, container, false)
val childContainer = view.findViewById<ViewGroup>(R.id.home_changeHandlerFrameLayout)
childRouter = getChildRouter(childContainer)
val bottomNavigationView = view.findViewById<BottomNavigationView>(R.id.navigation)
bottomNavigationView.setOnNavigationItemSelectedListener(this)
// We have not a single bundle/state saved.
// Looks like this [HomeController] was created for the first time
if (routerStates.size() == 0) {
// Select the first item
currentSelectedItemId = R.id.navigation_overview
childRouter.setRoot(RouterTransaction.with(OverviewController()))
} else {
// We have something in the back stack. Maybe an orientation change happen?
// We can just rebind the current router
childRouter.rebindIfNeeded()
}
return view
}
/**
* Listener which get called if a item from the [BottomNavigationView] is selected
*/
override fun onNavigationItemSelected(item: MenuItem): Boolean {
// Save the state from the current tab so that we can restore it later - if needed
saveStateFromCurrentTab(currentSelectedItemId)
currentSelectedItemId = item.itemId
// Clear all the hierarchy and backstack from the router. We have saved it already in the [routerStates]
clearStateFromChildRouter()
// Try to restore the state from the new selected tab.
val bundleState = tryToRestoreStateFromNewTab(currentSelectedItemId)
if (bundleState is Bundle) {
// We have found a state (hierarchy/backstack etc.) and can just restore it to the [childRouter]
childRouter.restoreInstanceState(bundleState)
childRouter.rebindIfNeeded()
return true
}
// There is no state (hierarchy/backstack etc.) saved in the [routerBundles].
// We have to create a new [Controller] and set as root
when (item.itemId) {
R.id.navigation_overview -> {
childRouter.setRoot(RouterTransaction.with(ConductorController("overview")))
return true
}
R.id.navigation_lineup -> {
childRouter.setRoot(RouterTransaction.with(ConductorController("lineup")))
return true
}
R.id.navigation_players -> {
childRouter.setRoot(RouterTransaction.with(ConductorController("players")))
return true
}
R.id.navigation_more -> {
childRouter.setRoot(RouterTransaction.with(ConductorController("more")))
return true
}
else -> return false
}
}
/**
* Try to restore the state (which was saved via [saveStateFromCurrentTab]) from the [routerStates].
*
* @return either a valid [Bundle] state or null if no state is available
*/
private fun tryToRestoreStateFromNewTab(itemId: Int): Bundle? {
return routerStates.get(itemId)
}
/**
* This will clear the state (hierarchy/backstack etc.) from the [childRouter] and goes back to root.
*/
private fun clearStateFromChildRouter() {
childRouter.setPopsLastView(true); /* Ensure the last view can be removed while we do this */
childRouter.popToRoot();
childRouter.popCurrentController();
childRouter.setPopsLastView(false);
}
/**
* This will save the current state of the tab (hierarchy/backstack etc.) from the [childRouter] in a [Bundle]
* and put it into the [routerStates] with the tab id as key
*/
private fun saveStateFromCurrentTab(itemId: Int) {
val routerBundle = Bundle()
childRouter.saveInstanceState(routerBundle)
routerStates.put(itemId, routerBundle)
}
/**
* Save our [routerStates] into the instanceState so we don't loose them on orientation change
*/
override fun onSaveInstanceState(outState: Bundle) {
saveStateFromCurrentTab(currentSelectedItemId)
outState.putSparseParcelableArray(ROUTER_STATES_KEY, routerStates)
super.onSaveInstanceState(outState)
}
/**
* Restore our [routerStates]
*/
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
routerStates = savedInstanceState.getSparseParcelableArray(ROUTER_STATES_KEY)
}
}
private const val CONDUCT_TEXT = "ARG"
class ConductorController(text: String = "") : Controller(Bundle().apply { putString(CONDUCT_TEXT, text) }) {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup): View {
val button = AppCompatButton(container.context)
button.text = args.getString(CONDUCT_TEXT)
button.setOnClickListener {
router.pushController(RouterTransaction.with(ConductorController(args.getString(CONDUCT_TEXT) + " child")))
}
return button
}
}
/**
* @return The *Intent* to start [MainNavigationActivity]. This *Intent* clears the activity back stack.
*/
fun Context.mainNavigationActivityIntent(): Intent {
val intent = Intent(this, MainNavigationActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return intent
}
class MainNavigationActivity : AppCompatActivity() {
private lateinit var router: Router
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_navigation)
val container = findViewById<ViewGroup>(R.id.changeHandlerFrameLayout)
router = Conductor.attachRouter(this, container, savedInstanceState)
if (!router.hasRootController()) {
router.setRoot(RouterTransaction.with(HomeController()))
}
}
override fun onBackPressed() {
if (!router.handleBack()) {
super.onBackPressed()
}
}
}
@swapnilsinha17
Copy link

How we can hide the bottom nav on child controller?as well show it on main controller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment