Skip to content

Instantly share code, notes, and snippets.

@NikolaDespotoski
Created October 20, 2016 17:16
Show Gist options
  • Save NikolaDespotoski/a201165e1a3f0b6cc57852559843b0dc to your computer and use it in GitHub Desktop.
Save NikolaDespotoski/a201165e1a3f0b6cc57852559843b0dc to your computer and use it in GitHub Desktop.
Updated bottom navigation behavior for BottomNavigationView
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<V extends View> extends VerticalScrollingBehavior<V> {
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 <V extends View> BottomNavigationBehavior<V> 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<V>) 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(100);
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
);
}
}
}
}
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<V extends View> extends CoordinatorLayout.Behavior<V> {
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);
}
}
@NikolaDespotoski
Copy link
Author

@laszlo-galosi Sorry for delayed response. Yes, Behavior MUST find the bottomnavigationview so it can set 8dp or do it in your xml.

Cheers,

Copy link

ghost commented Dec 7, 2016

Hi Nikola, thanks for the great work, I wonder how can we expand the bottom bar manually with some method similar to appbar.setExpanded(boolean)?

[Updated]

I just wrote the method below based on your code:

private void expandBottomBar(View bottomBar){
    if (ViewUnit.getWindowHeight(context) < (int) bottomBar.getY() + bottomBar.getHeight()){
        ViewCompat.animate(bottomBar).setDuration(100)
            .setInterpolator(BottomNavigationBehavior.INTERPOLATOR)
            .translationY(0).start();
    }
}

@NikolaDespotoski
Copy link
Author

NikolaDespotoski commented Dec 15, 2016

@ghost There is a method setHidden(V view, boolean bottomLayoutHidden) it can be accessed:

BottomNavigationBehavior behavior = BottomNavigationBehavior.from(mBottomNavigationView);
if(behavior != null){
   behavior.setHidden(mBottomNavigationView, true); //or false
}

@ninovanhooff
Copy link

I believe animateOffset should be called with +view.getHeight() when bottomLayoutHidden == true
as per te following fix

public void setHidden(V view, boolean bottomLayoutHidden) {
        if (!bottomLayoutHidden && hidden) {
            animateOffset(view, 0);
        } else if (bottomLayoutHidden && !hidden) {
            animateOffset(view, view.getHeight());
        }
        hidden = bottomLayoutHidden;
    }

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