From 1110a5503131e6c332bde77dfde29522a93868f1 Mon Sep 17 00:00:00 2001 From: "Christian H.C" Date: Mon, 19 Jun 2017 10:50:21 +0200 Subject: [PATCH 1/5] Adds BottomNavigationController to the demo app Showcasing a BottomNavigationView implementation with separate, retained backstacks for each MenuItem. --- .../BottomNavigationController.java | 209 ++++++++++++++++++ .../controllers/DepthChildController.java | 66 ++++++ .../demo/controllers/HomeController.java | 7 + .../layout/controller_bottom_navigation.xml | 24 ++ .../res/layout/controller_depth_child.xml | 26 +++ demo/src/main/res/menu/navigation.xml | 30 +++ demo/src/main/res/values/colors.xml | 1 + demo/src/main/res/values/strings.xml | 7 + 8 files changed, 370 insertions(+) create mode 100644 demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/BottomNavigationController.java create mode 100644 demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DepthChildController.java create mode 100644 demo/src/main/res/layout/controller_bottom_navigation.xml create mode 100644 demo/src/main/res/layout/controller_depth_child.xml create mode 100644 demo/src/main/res/menu/navigation.xml diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/BottomNavigationController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/BottomNavigationController.java new file mode 100644 index 00000000..eec7b238 --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/BottomNavigationController.java @@ -0,0 +1,209 @@ +package com.bluelinelabs.conductor.demo.controllers; + +import android.graphics.Color; +import android.os.Bundle; +import android.support.annotation.MenuRes; +import android.support.annotation.NonNull; +import android.support.design.widget.BottomNavigationView; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.bluelinelabs.conductor.ChangeHandlerFrameLayout; +import com.bluelinelabs.conductor.Controller; +import com.bluelinelabs.conductor.Router; +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.demo.R; +import com.bluelinelabs.conductor.demo.controllers.base.BaseController; +import com.bluelinelabs.conductor.demo.util.BundleBuilder; + +import butterknife.BindView; + +/** + * The {@link Controller} for the Bottom Navigation View. + * Populates a {@link BottomNavigationView} with the supplied {@link Menu} resource. + * The first item set as checked will be shown by default. + * The backstack of each {@link MenuItem} is switched out, in order to maintain a separate + * backstack for each {@link MenuItem} - even though that is against the Google Design Guidelines: + * + * @see Material + * Design Guidelines + * + * @author chris6647@gmail.com + */ +public class BottomNavigationController extends BaseController { + + public static final String TAG = "BottomNavigationController"; + + private static final String KEY_MENU_RESOURCE = "key_menu_resource"; + private static final String KEY_STATE_ROUTER_BUNDLES = "key_state_router_bundles"; + private static final String KEY_STATE_CURRENTLY_SELECTED_ID = "key_state_currently_selected_id"; + + @BindView(R.id.bottom_navigation_root) + LinearLayout bottomNavigationRoot; + + @BindView(R.id.navigation) + BottomNavigationView bottomNavigationView; + + @BindView(R.id.bottom_navigation_controller_container) + ChangeHandlerFrameLayout controllerContainer; + + private int currentlySelectedItemId; + + private SparseArray routerBundles; + + private Router childRouter; + + public BottomNavigationController(@MenuRes int menu) { + this(new BundleBuilder(new Bundle()).putInt(KEY_MENU_RESOURCE, menu).build()); + } + + public BottomNavigationController(Bundle args) { + super(args); + } + + @NonNull + @Override + protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return inflater.inflate(R.layout.controller_bottom_navigation, container, false); + } + + @Override + protected void onViewBound(@NonNull View view) { + super.onViewBound(view); + + /* Setup the BottomNavigationView with the constructor supplied Menu resource */ + bottomNavigationView.inflateMenu(getMenuResource()); + + Menu menu = bottomNavigationView.getMenu(); + int menuSize = menu.size(); + + childRouter = getChildRouter(controllerContainer); + + /* + * Not having access to Backstack or RouterTransaction constructors, + * we have to save/restore the entire routers for each backstack. + */ + if (routerBundles == null) { + routerBundles = new SparseArray<>(menuSize); + for (int i = 0; i < menuSize; i++) { + MenuItem menuItem = menu.getItem(i); + int itemId = menuItem.getItemId(); + /* Ensure the first checked item is shown */ + if (menuItem.isChecked()) { + childRouter.setRoot(RouterTransaction.with(BottomNavigationController.getControllerFor( + itemId))); + bottomNavigationView.setSelectedItemId(itemId); + currentlySelectedItemId = bottomNavigationView.getSelectedItemId(); + break; + } + } + } else { + /* + * Since we are restoring our state, + * and onRestoreInstanceState is called before onViewBound, + * all we need to do is rebind. + */ + childRouter.rebindIfNeeded(); + } + + bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { + if (currentlySelectedItemId != item.getItemId()) { + saveChildRouter(currentlySelectedItemId); + clearChildRouter(); + + currentlySelectedItemId = item.getItemId(); + Bundle routerBundle = routerBundles.get(currentlySelectedItemId); + if (routerBundle != null && !routerBundle.isEmpty()) { + childRouter.restoreInstanceState(routerBundle); + childRouter.rebindIfNeeded(); + } else { + childRouter.setRoot(RouterTransaction.with(BottomNavigationController.getControllerFor( + currentlySelectedItemId))); + } + return true; + } else { + return false; + } + } + }); + } + + private void saveChildRouter(int itemId) { + Bundle routerBundle = new Bundle(); + childRouter.saveInstanceState(routerBundle); + routerBundles.put(itemId, routerBundle); + } + + /** + * Removes ALL {@link Controller}'s in the child{@link Router}'s backstack + */ + private void clearChildRouter() { + childRouter.setPopsLastView(true); /* Ensure the last view can be removed while we do this */ + childRouter.popToRoot(); + childRouter.popCurrentController(); + childRouter.setPopsLastView(false); + } + + private int getMenuResource() { + return getArgs().getInt(KEY_MENU_RESOURCE); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + routerBundles = savedInstanceState.getSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES); + currentlySelectedItemId = savedInstanceState.getInt(KEY_STATE_CURRENTLY_SELECTED_ID); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + saveChildRouter(currentlySelectedItemId); + outState.putSparseParcelableArray(KEY_STATE_ROUTER_BUNDLES, routerBundles); + /* + * For some reason the BottomNavigationView does not seem to correctly restore its + * selectedId, even though the view appears with the correct state. + * So we keep track of it manually + */ + outState.putInt(KEY_STATE_CURRENTLY_SELECTED_ID, currentlySelectedItemId); + } + + @Override + public boolean handleBack() { + /* + * The childRouter should handleBack, + * as this BottomNavigationController doesn't have a back step sensible to the user. + */ + return childRouter.handleBack(); + } + + private static Controller getControllerFor(int menuItemId) { + Controller controller; + switch (menuItemId) { + case R.id.navigation_home: + controller = new DepthChildController(Color.BLUE, 0); + break; + case R.id.navigation_friends: + controller = new DepthChildController(Color.RED, 0); + break; + case R.id.navigation_intro: + controller = new DepthChildController(Color.YELLOW, 0); + break; + case R.id.navigation_planner: + controller = new DepthChildController(Color.GRAY, 0); + break; + case R.id.navigation_settings: + controller = new DepthChildController(Color.GREEN, 0); + break; + default: + throw new IllegalStateException( + "Unknown bottomNavigationView item selected."); + } + return controller; + } +} \ No newline at end of file diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DepthChildController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DepthChildController.java new file mode 100644 index 00000000..5ee5e903 --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/DepthChildController.java @@ -0,0 +1,66 @@ +package com.bluelinelabs.conductor.demo.controllers; + +import android.os.Bundle; + + +import android.annotation.SuppressLint; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.bluelinelabs.conductor.RouterTransaction; +import com.bluelinelabs.conductor.demo.R; +import com.bluelinelabs.conductor.demo.controllers.base.BaseController; +import com.bluelinelabs.conductor.demo.util.BundleBuilder; + +import butterknife.BindView; +import butterknife.OnClick; +/** + * The {@link com.bluelinelabs.conductor.Controller} for showing the depth and instance + * @author chris6647@gmail.com + */ +public class DepthChildController extends BaseController { + + public static final String TAG = "DepthChildController"; + + @BindView(R.id.message) + TextView textMessage; + + @OnClick(R.id.deeper) + void onClicked(){ + getRouter().pushController(RouterTransaction.with(new DepthChildController(getColor(), getDepth()+1))); + } + + public DepthChildController(@ColorInt int color, int depth) { + this(new BundleBuilder(new Bundle()).putInt("color", color).putInt("depth", depth).build()); + } + + public DepthChildController(Bundle args) { + super(args); + } + + @ColorInt int getColor(){ + return getArgs().getInt("color"); + } + + int getDepth(){ + return getArgs().getInt("depth"); + } + + @NonNull + @Override + protected View inflateView(@NonNull LayoutInflater inflater, @NonNull ViewGroup container) { + return inflater.inflate(R.layout.controller_depth_child, container, false); + } + + @SuppressLint("SetTextI18n") + @Override + protected void onViewBound(@NonNull View view) { + super.onViewBound(view); + view.setBackgroundColor(getColor()); + textMessage.setText("depth="+getDepth() + " | " +this.toString()); + } +} diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/HomeController.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/HomeController.java index c265be90..fddcd963 100644 --- a/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/HomeController.java +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/HomeController.java @@ -52,6 +52,7 @@ private enum DemoModel { MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.deep_orange_300), MASTER_DETAIL("Master Detail", R.color.grey_300), DRAG_DISMISS("Drag Dismiss", R.color.lime_300), + BOTTOM_NAVIGATION("Bottom Navigation", R.color.purple_600), EXTERNAL_MODULES("Bonus Modules", R.color.teal_300); String title; @@ -201,6 +202,12 @@ void onModelRowClick(DemoModel model, int position) { .pushChangeHandler(new FadeChangeHandler(false)) .popChangeHandler(new FadeChangeHandler())); break; + case BOTTOM_NAVIGATION: + getRouter().pushController(RouterTransaction.with(new BottomNavigationController + (R.menu.navigation)) + .pushChangeHandler(new FadeChangeHandler()) + .popChangeHandler(new FadeChangeHandler())); + break; case EXTERNAL_MODULES: getRouter().pushController(RouterTransaction.with(new ExternalModulesController()) .pushChangeHandler(new HorizontalChangeHandler()) diff --git a/demo/src/main/res/layout/controller_bottom_navigation.xml b/demo/src/main/res/layout/controller_bottom_navigation.xml new file mode 100644 index 00000000..e34ccec8 --- /dev/null +++ b/demo/src/main/res/layout/controller_bottom_navigation.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/layout/controller_depth_child.xml b/demo/src/main/res/layout/controller_depth_child.xml new file mode 100644 index 00000000..d5d95d67 --- /dev/null +++ b/demo/src/main/res/layout/controller_depth_child.xml @@ -0,0 +1,26 @@ + + + + + +