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..3ddc32df --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/controllers/BottomNavigationController.java @@ -0,0 +1,84 @@ +package com.bluelinelabs.conductor.demo.controllers; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +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.widget.NonScrollableViewPager; +import com.bluelinelabs.conductor.viewpager.RouterPagerAdapter; +import com.google.android.material.bottomnavigation.BottomNavigationView; + +import butterknife.BindView; + +public class BottomNavigationController extends BaseController { + + @BindView(R.id.view_pager) NonScrollableViewPager viewPager; + @BindView(R.id.navigation) BottomNavigationView navigation; + + private final RouterPagerAdapter pagerAdapter; + + public BottomNavigationController() { + pagerAdapter = new RouterPagerAdapter(this) { + @Override + public void configureRouter(@NonNull Router router, int position) { + if (!router.hasRootController()) { + Controller page = new NavigationDemoController(position * 10, NavigationDemoController.DisplayUpMode.HIDE); + router.setRoot(RouterTransaction.with(page)); + } + } + + @Override + public int getCount() { + return 4; + } + + @Override + public CharSequence getPageTitle(int position) { + return "Tab " + position; + } + }; + } + + @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); + viewPager.setAdapter(pagerAdapter); + navigation.setOnNavigationItemSelectedListener(menuItem -> { + int index = 0; + switch (menuItem.getItemId()) { + case R.id.home: + index = 0; + break; + case R.id.places: + index = 1; + break; + case R.id.favorite: + index = 2; + break; + case R.id.settings: + index = 3; + break; + } + viewPager.setCurrentItem(index); + return true; + }); + } + + @Override + protected void onDestroyView(@NonNull View view) { + viewPager.setAdapter(null); + super.onDestroyView(view); + } +} 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 ad1a53e7..950c3658 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 @@ -48,6 +48,7 @@ private enum DemoModel { SHARED_ELEMENT_TRANSITIONS("Shared Element Demos", R.color.purple_300), CHILD_CONTROLLERS("Child Controllers", R.color.orange_300), VIEW_PAGER("ViewPager", R.color.green_300), + BOTTOM_NAVIGATION("Bottom Navigation", R.color.brown_300), TARGET_CONTROLLER("Target Controller", R.color.pink_300), MULTIPLE_CHILD_ROUTERS("Multiple Child Routers", R.color.deep_orange_300), MASTER_DETAIL("Master Detail", R.color.grey_300), @@ -186,6 +187,11 @@ void onModelRowClick(DemoModel model, int position) { .pushChangeHandler(new FadeChangeHandler()) .popChangeHandler(new FadeChangeHandler())); break; + case BOTTOM_NAVIGATION: + getRouter().pushController(RouterTransaction.with(new BottomNavigationController()) + .pushChangeHandler(new FadeChangeHandler()) + .popChangeHandler(new FadeChangeHandler())); + break; case CHILD_CONTROLLERS: getRouter().pushController(RouterTransaction.with(new ParentController()) .pushChangeHandler(new FadeChangeHandler()) diff --git a/demo/src/main/java/com/bluelinelabs/conductor/demo/widget/NonScrollableViewPager.java b/demo/src/main/java/com/bluelinelabs/conductor/demo/widget/NonScrollableViewPager.java new file mode 100644 index 00000000..b709a517 --- /dev/null +++ b/demo/src/main/java/com/bluelinelabs/conductor/demo/widget/NonScrollableViewPager.java @@ -0,0 +1,635 @@ +package com.bluelinelabs.conductor.demo.widget; + +import android.content.Context; +import android.database.DataSetObserver; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.customview.view.AbsSavedState; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import java.util.ArrayList; +import java.util.List; + +public class NonScrollableViewPager extends ViewGroup { + + PagerAdapter mAdapter; + private ItemInfo mCurrentItem; + int mCurItem; // Index of currently displayed page. + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + private DataSetObserver mObserver = new DataSetObserver() { + @Override + public void onChanged() { + dataSetChanged(); + } + + @Override + public void onInvalidated() { + dataSetChanged(); + } + }; + + private boolean mInLayout; + private boolean mFirstLayout = true; + + static class ItemInfo { + Object object; + int position; + } + + private List mOnPageChangeListeners; + + public NonScrollableViewPager(@NonNull Context context) { + super(context); + initViewPager(); + } + + public NonScrollableViewPager(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + initViewPager(); + } + + void initViewPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + + ViewCompat.setOnApplyWindowInsetsListener(this, + new androidx.core.view.OnApplyWindowInsetsListener() { + private final Rect mTempRect = new Rect(); + + @Override + public WindowInsetsCompat onApplyWindowInsets(final View v, + final WindowInsetsCompat originalInsets) { + // First let the ViewPager itself try and consume them... + final WindowInsetsCompat applied = + ViewCompat.onApplyWindowInsets(v, originalInsets); + if (applied.isConsumed()) { + // If the ViewPager consumed all insets, return now + return applied; + } + + // Now we'll manually dispatch the insets to our children. Since ViewPager + // children are always full-height, we do not want to use the standard + // ViewGroup dispatchApplyWindowInsets since if child 0 consumes them, + // the rest of the children will not receive any insets. To workaround this + // we manually dispatch the applied insets, not allowing children to + // consume them from each other. We do however keep track of any insets + // which are consumed, returning the union of our children's consumption + final Rect res = mTempRect; + res.left = applied.getSystemWindowInsetLeft(); + res.top = applied.getSystemWindowInsetTop(); + res.right = applied.getSystemWindowInsetRight(); + res.bottom = applied.getSystemWindowInsetBottom(); + + for (int i = 0, count = getChildCount(); i < count; i++) { + final WindowInsetsCompat childInsets = ViewCompat + .dispatchApplyWindowInsets(getChildAt(i), applied); + // Now keep track of any consumed by tracking each dimension's min + // value + res.left = Math.min(childInsets.getSystemWindowInsetLeft(), + res.left); + res.top = Math.min(childInsets.getSystemWindowInsetTop(), + res.top); + res.right = Math.min(childInsets.getSystemWindowInsetRight(), + res.right); + res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(), + res.bottom); + } + + // Now return a new WindowInsets, using the consumed window insets + return applied.replaceSystemWindowInsets( + res.left, res.top, res.right, res.bottom); + } + }); + } + + /** + * Add a listener that will be invoked whenever the page changes or is incrementally + * scrolled. See {@link ViewPager.OnPageChangeListener}. + * + *

Components that add a listener should take care to remove it when finished. + * Other components that take ownership of a view may call {@link #clearOnPageChangeListeners()} + * to remove all attached listeners.

+ * + * @param listener listener to add + */ + public void addOnPageChangeListener(@NonNull ViewPager.OnPageChangeListener listener) { + if (mOnPageChangeListeners == null) { + mOnPageChangeListeners = new ArrayList<>(); + } + mOnPageChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnPageChangeListener(ViewPager.OnPageChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnPageChangeListener(@NonNull ViewPager.OnPageChangeListener listener) { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.remove(listener); + } + } + + /** + * Remove all listeners that are notified of any changes in scroll state or position. + */ + public void clearOnPageChangeListeners() { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.clear(); + } + } + + private void dispatchOnPageSelected(int position) { + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + ViewPager.OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageSelected(position); + } + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + @Nullable + public PagerAdapter getAdapter() { + return mAdapter; + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(@Nullable PagerAdapter adapter) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + mAdapter.startUpdate(this); + if (mCurrentItem != null) { + mAdapter.destroyItem(this, mCurrentItem.position, mCurrentItem.object); + mCurrentItem = null; + } + mAdapter.finishUpdate(this); + removeAllViews(); + mCurItem = 0; + } + + mAdapter = adapter; + + if (mAdapter != null) { + mAdapter.registerDataSetObserver(mObserver); + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + setCurrentItemInternal(item, false); + } + + public int getCurrentItem() { + return mCurItem; + } + + void setCurrentItemInternal(int item, boolean always) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + return; + } + + if (!always && mCurItem == item && mCurrentItem != null) { + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final boolean dispatchSelected = mCurItem != item; + + if (mFirstLayout) { + // We don't have any idea how big we are yet and shouldn't have any pages either. + // Just set things up and let the pending layout handle things. + mCurItem = item; + if (dispatchSelected) { + dispatchOnPageSelected(item); + } + requestLayout(); + } else { + populate(item); + if (dispatchSelected) { + dispatchOnPageSelected(item); + } + } + } + + void dataSetChanged() { + final int adapterCount = mAdapter.getCount(); + boolean needPopulate = false; + int newCurrItem = mCurItem; + + if (mCurrentItem != null) { + final int newPos = mAdapter.getItemPosition(mCurrentItem.object); + if (newPos == PagerAdapter.POSITION_NONE) { + needPopulate = true; + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + } + } else { + needPopulate = true; + } + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.needsPositionUpdate = true; + } + + setCurrentItemInternal(newCurrItem, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + mCurItem = newCurrentItem; + + if (mAdapter == null) { + return; + } + + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + if (mCurrentItem != null) { + if (mCurrentItem.position != newCurrentItem) { + mAdapter.destroyItem(this, mCurrentItem.position, mCurrentItem.object); + mCurrentItem = null; + } + } + if (mCurrentItem == null) { + mCurrentItem = addNewItem(newCurrentItem); + } + + mAdapter.setPrimaryItem(this, mCurItem, mCurrentItem.object); + + mAdapter.finishUpdate(this); + + // Check width measurement of current pages and drawing sort order. + // Update LayoutParams as needed. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + lp.childIndex = i; + if (lp.needsPositionUpdate) { + final ItemInfo ii = infoForChild(child); + if (ii != null) { + lp.position = ii.position; + } + } + } + + if (hasFocus()) { + View currentFocused = findFocus(); + ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; + if (ii == null || ii.position != mCurItem) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(View.FOCUS_FORWARD)) { + break; + } + } + } + } + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + if (mInLayout) { + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + ItemInfo infoForChild(View child) { + if (mCurrentItem != null) { + if (mAdapter.isViewFromObject(child, mCurrentItem.object)) { + return mCurrentItem; + } + } + return null; + } + + ItemInfo infoForAnyChild(View child) { + ViewParent parent; + while ((parent = child.getParent()) != this) { + if (!(parent instanceof View)) { + return null; + } + child = (View) parent; + } + return infoForChild(child); + } + + ItemInfo addNewItem(int position) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + return ii; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + return false; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // For simple implementation, our internal size is always 0. + // We depend on the container to specify the layout size of + // our view. We can't really know what it is since we will be + // adding and removing different arbitrary views and do not + // want the layout to change as this happens. + setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), + getDefaultSize(0, heightMeasureSpec)); + + final int measuredWidth = getMeasuredWidth(); + + // Children are just made to fill our space. + int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); + int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); + + int mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); + int mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); + + + // Make sure we have created all fragments that we need to have shown. + mInLayout = true; + populate(); + mInLayout = false; + + // Page views next. + int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (infoForChild(child) != null) { + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + childWidth, + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + (height - paddingTop - paddingBottom), + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + child.layout(paddingLeft, paddingTop, + paddingLeft + child.getMeasuredWidth(), + paddingTop + child.getMeasuredHeight()); + } + } + } + + mFirstLayout = false; + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + /** + * This is the persistent state that is saved by ViewPager. Only needed + * if you are creating a sublass of ViewPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends AbsSavedState { + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(@NonNull Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "NonScrollableViewPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + + public static final Creator CREATOR = new ClassLoaderCreator() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + + SavedState(Parcel in, ClassLoader loader) { + super(in, loader); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + } + + /** + * Layout parameters that should be supplied for views added to a + * ViewPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if data set changed and child needs its position to be updated + */ + boolean needsPositionUpdate; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the ViewPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(MATCH_PARENT, MATCH_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + } + } +} \ No newline at end of file diff --git a/demo/src/main/res/drawable/ic_home_black_24dp.xml b/demo/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100644 index 00000000..70fb2910 --- /dev/null +++ b/demo/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/demo/src/main/res/drawable/ic_settings_black_24dp.xml b/demo/src/main/res/drawable/ic_settings_black_24dp.xml new file mode 100644 index 00000000..24a5623c --- /dev/null +++ b/demo/src/main/res/drawable/ic_settings_black_24dp.xml @@ -0,0 +1,9 @@ + + + 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..c680b64c --- /dev/null +++ b/demo/src/main/res/layout/controller_bottom_navigation.xml @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/menu/navigation.xml b/demo/src/main/res/menu/navigation.xml new file mode 100644 index 00000000..84c33e2d --- /dev/null +++ b/demo/src/main/res/menu/navigation.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index f359294d..5fceefd1 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -14,6 +14,12 @@ Next Controller Controller #%1$d + + Home + Places + Favorite + Settings + Next