Skip to content

Instantly share code, notes, and snippets.

@wakim
Created May 1, 2017 16:35
Show Gist options
  • Save wakim/6582051b416cde29900fc10b7067f72a to your computer and use it in GitHub Desktop.
Save wakim/6582051b416cde29900fc10b7067f72a to your computer and use it in GitHub Desktop.
Improved implementation of BottomSheet Behavior
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
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.view.View;
import android.view.animation.Interpolator;
import br.com.egasosa.R;
import br.com.egasosa.extensions.ContextExtensionsKt;
/**
* Created by Nikola D. on 3/15/2016.
*
* Credit goes to Nikola Despotoski:
* https://github.com/NikolaDespotoski
*/
@Keep
public class BottomNavigationBehavior<V extends View> extends VerticalScrollingBehavior<V> {
public static final Interpolator INTERPOLATOR = new LinearOutSlowInInterpolator();
private final BottomNavigationWithSnackbar mWithSnackBarImpl = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? new LollipopBottomNavWithSnackBarImpl() : new PreLollipopBottomNavWithSnackBarImpl();
private ViewPropertyAnimatorCompat mTranslationAnimator;
private int snackbarHeight = 0;
private boolean hidden = false;
private boolean scrollingEnabled = true;
public BottomNavigationBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
mWithSnackBarImpl.updateSnackbar(parent, dependency, child);
return dependency instanceof Snackbar.SnackbarLayout;
}
@Override
public void onNestedVerticalOverScroll(CoordinatorLayout coordinatorLayout, V child, @ScrollDirection int direction, int currentOverScroll, int totalOverScroll) {}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
updateScrollingForSnackbar(dependency, true);
mWithSnackBarImpl.snackbarRemoved(child);
super.onDependentViewRemoved(parent, child, dependency);
}
private void updateScrollingForSnackbar(View dependency, boolean enabled) {
if (dependency instanceof Snackbar.SnackbarLayout) {
scrollingEnabled = enabled;
}
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
updateScrollingForSnackbar(dependency, false);
return super.onDependentViewChanged(parent, child, dependency);
}
private int accumulatedY = 0;
@Override
public void onDirectionNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx, int dy, int[] consumed, @ScrollDirection int scrollDirection) {
int dySignal = (dy >= 0 ? 1 : -1);
int accumulatedSignal = (accumulatedY >= 0 ? 1 : -1);
final int offsetThreshold = child.getHeight();
if (dySignal != accumulatedSignal) {
accumulatedY = 0;
}
accumulatedY += dy;
if (accumulatedY > offsetThreshold || accumulatedY < -offsetThreshold) {
handleDirection(child, scrollDirection);
}
}
private void handleDirection(V child, int scrollDirection) {
if (!scrollingEnabled) return;
if (!child.isEnabled()) 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);
mTranslationAnimator.translationY(offset).start();
}
private void ensureOrCancelAnimator(V child) {
if (mTranslationAnimator == null) {
mTranslationAnimator = ViewCompat.animate(child);
mTranslationAnimator.setDuration(300);
mTranslationAnimator.setInterpolator(INTERPOLATOR);
} else {
mTranslationAnimator.cancel();
}
}
@SuppressWarnings("unused")
void setHidden(@NonNull 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);
void snackbarRemoved(View child);
}
private class PreLollipopBottomNavWithSnackBarImpl implements BottomNavigationWithSnackbar {
@Override
public void updateSnackbar(CoordinatorLayout parent, View dependency, View child) {
if (dependency instanceof Snackbar.SnackbarLayout) {
if (snackbarHeight == 0) {
snackbarHeight = dependency.getHeight();
}
if (ViewCompat.getTranslationY(child) != 0) return;
int targetPadding = child.getMeasuredHeight() + snackbarHeight;
child.bringToFront();
dependency.setPadding(dependency.getPaddingLeft(), dependency.getPaddingTop(), dependency.getPaddingRight(), targetPadding);
}
}
@Override
public void snackbarRemoved(View child) {}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private class LollipopBottomNavWithSnackBarImpl implements BottomNavigationWithSnackbar {
@Override
public void updateSnackbar(CoordinatorLayout parent, View dependency, View child) {
if (dependency instanceof Snackbar.SnackbarLayout) {
if (snackbarHeight == 0) {
snackbarHeight = dependency.getHeight();
}
if (ViewCompat.getTranslationY(child) != 0) return;
int targetPadding = snackbarHeight + child.getMeasuredHeight();
child.setTag(R.dimen.design_snackbar_elevation, child.getElevation());
child.setElevation(ContextExtensionsKt.dp(10, child.getContext()));
dependency.setPadding(dependency.getPaddingLeft(), dependency.getPaddingTop(), dependency.getPaddingRight(), targetPadding);
}
}
@Override
public void snackbarRemoved(View child) {
Object tag = child.getTag(R.dimen.design_snackbar_elevation);
if (tag instanceof Float) {
child.setElevation((float) tag);
}
}
}
}
import android.content.Context;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.view.WindowInsetsCompat;
import android.util.AttributeSet;
import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
// From: https://medium.com/@nullthemall/bottom-navigation-behavior-388b9b206667#.9b1qpbcbz
/**
* Created by Nikola D. on 3/15/2016.
*/
public abstract class VerticalScrollingBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
private int mTotalDyUnconsumed = 0;
private int mTotalDy = -1;
@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
* @param 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 & View.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);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment