diff --git a/app/src/main/java/bottomnav/hitherejoe/com/bottomnavigationsample/BottomNavigationBehavior.java b/app/src/main/java/bottomnav/hitherejoe/com/bottomnavigationsample/BottomNavigationBehavior.java new file mode 100644 index 0000000..d8811fb --- /dev/null +++ b/app/src/main/java/bottomnav/hitherejoe/com/bottomnavigationsample/BottomNavigationBehavior.java @@ -0,0 +1,223 @@ +package bottomnav.hitherejoe.com.bottomnavigationsample; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.Snackbar; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewPropertyAnimatorCompat; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + +/** + * Created by Nikola D. on 3/15/2016. + */ +public final class BottomNavigationBehavior extends VerticalScrollingBehavior { + private static final Interpolator INTERPOLATOR = new LinearOutSlowInInterpolator(); + private final BottomNavigationWithSnackbar mWithSnackBarImpl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? new LollipopBottomNavWithSnackBarImpl() : new PreLollipopBottomNavWithSnackBarImpl(); + private int mTabLayoutId; + private boolean hidden = false; + private ViewPropertyAnimatorCompat mOffsetValueAnimator; + private ViewGroup mTabLayout; + private int mSnackbarHeight = -1; + private boolean scrollingEnabled = true; + private boolean hideAlongSnackbar = false; + int[] attrsArray = new int[]{ + android.R.attr.id, android.R.attr.elevation}; + private int mElevation = 8; + + public BottomNavigationBehavior() { + super(); + } + + public BottomNavigationBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, + attrsArray); + mTabLayoutId = a.getResourceId(0, View.NO_ID); + mElevation = a.getResourceId(1, (int) TypedValue + .applyDimension(TypedValue.COMPLEX_UNIT_DIP, mElevation, context.getResources().getDisplayMetrics())); + a.recycle(); + } + + public static BottomNavigationBehavior from(@NonNull V view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (!(params instanceof CoordinatorLayout.LayoutParams)) { + throw new IllegalArgumentException("The view is not a child of CoordinatorLayout"); + } + CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params) + .getBehavior(); + if (!(behavior instanceof BottomNavigationBehavior)) { + throw new IllegalArgumentException( + "The view is not associated with BottomNavigationBehavior"); + } + return (BottomNavigationBehavior) behavior; + } + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { + mWithSnackBarImpl.updateSnackbar(parent, dependency, child); + return dependency instanceof Snackbar.SnackbarLayout; + } + + @Override + public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) { + updateScrollingForSnackbar(dependency, child, true); + super.onDependentViewRemoved(parent, child, dependency); + } + + private void updateScrollingForSnackbar(View dependency, V child, boolean enabled) { + if (dependency instanceof Snackbar.SnackbarLayout) { + scrollingEnabled = enabled; + if (!hideAlongSnackbar && ViewCompat.getTranslationY(child) != 0) { + ViewCompat.setTranslationY(child, 0); + hidden = false; + hideAlongSnackbar = true; + } else if (hideAlongSnackbar) { + hidden = true; + animateOffset(child, -child.getHeight()); + } + } + } + + @Override + public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) { + updateScrollingForSnackbar(dependency, child, false); + return super.onDependentViewChanged(parent, child, dependency); + } + + @Override + public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) { + boolean layoutChild = super.onLayoutChild(parent, child, layoutDirection); + if (mTabLayout == null && mTabLayoutId != View.NO_ID) { + mTabLayout = findTabLayout(child); + elevateNavigationView(); + } + + return layoutChild; + } + + @Nullable + private ViewGroup findTabLayout(@NonNull View child) { + if (mTabLayoutId == 0) return null; + return (ViewGroup) child.findViewById(mTabLayoutId); + } + + @Override + public void onNestedVerticalOverScroll(CoordinatorLayout coordinatorLayout, V child, @ScrollDirection int direction, int currentOverScroll, int totalOverScroll) { + } + + @Override + public void onDirectionNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed, @ScrollDirection int scrollDirection) { + handleDirection(child, scrollDirection); + } + + private void handleDirection(V child, @ScrollDirection int scrollDirection) { + if (!scrollingEnabled) return; + if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_DOWN && hidden) { + hidden = false; + animateOffset(child, 0); + } else if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_UP && !hidden) { + hidden = true; + animateOffset(child, child.getHeight()); + } + } + + @Override + protected boolean onNestedDirectionFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, @ScrollDirection int scrollDirection) { + handleDirection(child, scrollDirection); + return true; + } + + private void animateOffset(final V child, final int offset) { + ensureOrCancelAnimator(child); + mOffsetValueAnimator.translationY(offset).start(); + } + + + + private void ensureOrCancelAnimator(@NonNull V child) { + if (mOffsetValueAnimator == null) { + mOffsetValueAnimator = ViewCompat.animate(child); + mOffsetValueAnimator.setDuration(250); + mOffsetValueAnimator.setInterpolator(INTERPOLATOR); + } else { + mOffsetValueAnimator.cancel(); + } + } + + private void elevateNavigationView() { + if (mTabLayout != null) { + ViewCompat.setElevation(mTabLayout, mElevation); + } + } + + public boolean isScrollingEnabled() { + return scrollingEnabled; + } + + public void setScrollingEnabled(boolean scrollingEnabled) { + this.scrollingEnabled = scrollingEnabled; + } + + public void setHidden(V view, boolean bottomLayoutHidden) { + if (!bottomLayoutHidden && hidden) { + animateOffset(view, 0); + } else if (bottomLayoutHidden && !hidden) { + animateOffset(view, -view.getHeight()); + } + hidden = bottomLayoutHidden; + } + + private interface BottomNavigationWithSnackbar { + void updateSnackbar(CoordinatorLayout parent, View dependency, View child); + } + + private class PreLollipopBottomNavWithSnackBarImpl implements BottomNavigationWithSnackbar { + + @Override + public void updateSnackbar(CoordinatorLayout parent, View dependency, View child) { + if (dependency instanceof Snackbar.SnackbarLayout) { + if (mSnackbarHeight == -1) { + mSnackbarHeight = dependency.getHeight(); + } + + int targetPadding = child.getMeasuredHeight(); + + int shadow = (int) ViewCompat.getElevation(child); + ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) dependency.getLayoutParams(); + layoutParams.bottomMargin = targetPadding - shadow; + child.bringToFront(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + child.getParent().requestLayout(); + ((View) child.getParent()).invalidate(); + } + + } + } + } + + private class LollipopBottomNavWithSnackBarImpl implements BottomNavigationWithSnackbar { + + @Override + public void updateSnackbar(CoordinatorLayout parent, View dependency, View child) { + if (dependency instanceof Snackbar.SnackbarLayout) { + if (mSnackbarHeight == -1) { + mSnackbarHeight = dependency.getHeight(); + } + int targetPadding = (mSnackbarHeight + + child.getMeasuredHeight()); + dependency.setPadding(dependency.getPaddingLeft(), + dependency.getPaddingTop(), dependency.getPaddingRight(), targetPadding + ); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/bottomnav/hitherejoe/com/bottomnavigationsample/VerticalScrollingBehavior.java b/app/src/main/java/bottomnav/hitherejoe/com/bottomnavigationsample/VerticalScrollingBehavior.java new file mode 100644 index 0000000..cfa17bc --- /dev/null +++ b/app/src/main/java/bottomnav/hitherejoe/com/bottomnavigationsample/VerticalScrollingBehavior.java @@ -0,0 +1,146 @@ +package bottomnav.hitherejoe.com.bottomnavigationsample; + +import android.content.Context; +import android.os.Parcelable; +import android.support.annotation.IntDef; +import android.support.design.widget.CoordinatorLayout; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.WindowInsetsCompat; +import android.util.AttributeSet; +import android.view.View; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Created by Nikola on 11/22/2015. + */ +public abstract class VerticalScrollingBehavior extends CoordinatorLayout.Behavior { + + private int mTotalDyUnconsumed = 0; + private int mTotalDy = 0; + @ScrollDirection + private int mOverScrollDirection = ScrollDirection.SCROLL_NONE; + @ScrollDirection + private int mScrollDirection = ScrollDirection.SCROLL_NONE; + + public VerticalScrollingBehavior(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public VerticalScrollingBehavior() { + super(); + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ScrollDirection.SCROLL_DIRECTION_UP, ScrollDirection.SCROLL_DIRECTION_DOWN}) + public @interface ScrollDirection { + int SCROLL_DIRECTION_UP = 1; + int SCROLL_DIRECTION_DOWN = -1; + int SCROLL_NONE = 0; + } + + + /* + @return Overscroll direction: SCROLL_DIRECTION_UP, CROLL_DIRECTION_DOWN, SCROLL_NONE + */ + @ScrollDirection + public int getOverScrollDirection() { + return mOverScrollDirection; + } + + + /** + * @return Scroll direction: SCROLL_DIRECTION_UP, SCROLL_DIRECTION_DOWN, SCROLL_NONE + */ + + @ScrollDirection + public int getScrollDirection() { + return mScrollDirection; + } + + + /** + * @param coordinatorLayout CoordinatoyLayout parent + * @param child View child + * @param direction Direction of the overscroll: SCROLL_DIRECTION_UP, SCROLL_DIRECTION_DOWN + * @param currentOverScroll Unconsumed value, negative or positive based on the direction; + * @param totalOverScroll Cumulative value for current direction + */ + public abstract void onNestedVerticalOverScroll(CoordinatorLayout coordinatorLayout, V child, @ScrollDirection int direction, int currentOverScroll, int totalOverScroll); + + /** + * @param scrollDirection Direction of the overscroll: SCROLL_DIRECTION_UP, SCROLL_DIRECTION_DOWN + */ + public abstract void onDirectionNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed, @ScrollDirection int scrollDirection); + + @Override + public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; + } + + @Override + public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) { + super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes); + } + + @Override + public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) { + super.onStopNestedScroll(coordinatorLayout, child, target); + } + + @Override + public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); + if (dyUnconsumed > 0 && mTotalDyUnconsumed < 0) { + mTotalDyUnconsumed = 0; + mOverScrollDirection = ScrollDirection.SCROLL_DIRECTION_UP; + } else if (dyUnconsumed < 0 && mTotalDyUnconsumed > 0) { + mTotalDyUnconsumed = 0; + mOverScrollDirection = ScrollDirection.SCROLL_DIRECTION_DOWN; + } + mTotalDyUnconsumed += dyUnconsumed; + onNestedVerticalOverScroll(coordinatorLayout, child, mOverScrollDirection, dyConsumed, mTotalDyUnconsumed); + } + + @Override + public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed) { + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed); + if (dy > 0 && mTotalDy < 0) { + mTotalDy = 0; + mScrollDirection = ScrollDirection.SCROLL_DIRECTION_UP; + } else if (dy < 0 && mTotalDy > 0) { + mTotalDy = 0; + mScrollDirection = ScrollDirection.SCROLL_DIRECTION_DOWN; + } + mTotalDy += dy; + onDirectionNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, mScrollDirection); + } + + + @Override + public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, boolean consumed) { + super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed); + mScrollDirection = velocityY > 0 ? ScrollDirection.SCROLL_DIRECTION_UP : ScrollDirection.SCROLL_DIRECTION_DOWN; + return onNestedDirectionFling(coordinatorLayout, child, target, velocityX, velocityY, mScrollDirection); + } + + protected abstract boolean onNestedDirectionFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY, @ScrollDirection int scrollDirection); + + @Override + public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target, float velocityX, float velocityY) { + return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY); + } + + @Override + public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, V child, WindowInsetsCompat insets) { + + return super.onApplyWindowInsets(coordinatorLayout, child, insets); + } + + @Override + public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) { + return super.onSaveInstanceState(parent, child); + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index eb3240c..4999d9d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -35,6 +35,7 @@ app:itemBackground="@color/colorPrimary" app:itemIconTint="@color/white" app:itemTextColor="@color/white" + app:layout_behavior=".BottomNavigationBehavior" app:menu="@menu/bottom_navigation_main" /> diff --git a/build.gradle b/build.gradle index a3330d4..c20bca1 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0' + classpath 'com.android.tools.build:gradle:2.2.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files