package it.sephiroth.android.library.bottomnavigation;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar.SnackbarLayout;
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.ViewConfiguration;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.animation.Interpolator;
import it.sephiroth.android.library.bottonnavigation.R;
import proguard.annotation.Keep;
import proguard.annotation.KeepClassMembers;
import static android.util.Log.DEBUG;
import static android.util.Log.INFO;
import static android.util.Log.WARN;
import static it.sephiroth.android.library.bottomnavigation.BottomNavigation.PENDING_ACTION_ANIMATE_ENABLED;
import static it.sephiroth.android.library.bottomnavigation.BottomNavigation.PENDING_ACTION_NONE;
import static it.sephiroth.android.library.bottomnavigation.MiscUtils.log;
/**
* Created by alessandro crugnola on 4/2/16.
* alessandro.crugnola@gmail.com
*/
@Keep
@KeepClassMembers
public class BottomBehavior extends VerticalScrollingBehavior<BottomNavigation> {
private static final String TAG = BottomBehavior.class.getSimpleName();
private boolean scrollable;
private boolean scrollEnabled;
private boolean enabled;
/**
* default hide/show interpolator
*/
private static final Interpolator INTERPOLATOR = new LinearOutSlowInInterpolator();
/**
* show/hide animation duration
*/
private final int animationDuration;
/**
* bottom inset when TRANSLUCENT_NAVIGATION is turned on
*/
private int bottomInset;
/**
* bottom navigation real height
*/
private int height;
/**
* maximum scroll offset
*/
private int maxOffset;
/**
* true if the current configuration has the TRANSLUCENT_NAVIGATION turned on
*/
private boolean translucentNavigation;
/**
* Minimum touch distance
*/
private final int scaledTouchSlop;
/**
* hide/show animator
*/
private ViewPropertyAnimatorCompat animator;
/**
* current visibility status
*/
private boolean hidden = false;
/**
* current Y offset
*/
private int offset;
private OnExpandStatusChangeListener listener;
protected SnackBarDependentView snackbarDependentView;
public BottomBehavior() {
this(null, null);
}
public BottomBehavior(final Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BottomNavigationBehavior);
this.scrollable = array.getBoolean(R.styleable.BottomNavigationBehavior_bbn_scrollEnabled, true);
this.scrollEnabled = true;
this.animationDuration = array.getInt(
R.styleable.BottomNavigationBehavior_bbn_animationDuration,
context.getResources().getInteger(R.integer.bbn_hide_animation_duration)
);
this.scaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2;
this.offset = 0;
array.recycle();
log(TAG, DEBUG, "scrollable: %b, duration: %d, touchSlop: %d", scrollable, animationDuration, scaledTouchSlop);
}
public void setOnExpandStatusChangeListener(final OnExpandStatusChangeListener listener) {
this.listener = listener;
}
public boolean isScrollable() {
return scrollable;
}
public void setScrollable(final boolean scrollable) {
this.scrollable = scrollable;
}
public boolean isExpanded() {
return !hidden;
}
public void setLayoutValues(final int bottomNavHeight, final int bottomInset) {
log(TAG, INFO, "setLayoutValues(%d, %d)", bottomNavHeight, bottomInset);
this.height = bottomNavHeight;
this.bottomInset = bottomInset;
this.translucentNavigation = bottomInset > 0;
this.maxOffset = height + (translucentNavigation ? bottomInset : 0);
this.enabled = true;
log(
TAG, DEBUG, "height: %d, translucent: %b, maxOffset: %d, bottomInset: %d", height, translucentNavigation, maxOffset,
bottomInset
);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, BottomNavigation child, View dependency) {
log(TAG, INFO, "layoutDependsOn: %s", dependency);
if (!enabled) {
return false;
}
return isSnackbar(dependency);
}
private boolean isSnackbar(@NonNull final View view) {
return SnackbarLayout.class.isInstance(view);
}
@Override
public boolean onLayoutChild(CoordinatorLayout parent, BottomNavigation abl, int layoutDirection) {
boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
final int pendingAction = abl.getPendingAction();
if (pendingAction != PENDING_ACTION_NONE) {
final boolean animate = (pendingAction & PENDING_ACTION_ANIMATE_ENABLED) != 0;
if ((pendingAction & BottomNavigation.PENDING_ACTION_COLLAPSED) != 0) {
setExpanded(parent, abl, false, animate);
} else {
if ((pendingAction & BottomNavigation.PENDING_ACTION_EXPANDED) != 0) {
setExpanded(parent, abl, true, animate);
}
}
// Finally reset the pending state
abl.resetPendingAction();
}
return handled;
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, BottomNavigation child, View dependency) {
if (isSnackbar(dependency)) {
if (null != snackbarDependentView) {
snackbarDependentView.onDestroy();
}
snackbarDependentView = null;
}
}
@Override
public boolean onDependentViewChanged(final CoordinatorLayout parent, final BottomNavigation child, View dependency) {
if (isSnackbar(dependency)) {
if (null == snackbarDependentView) {
snackbarDependentView = new SnackBarDependentView((SnackbarLayout) dependency, height, bottomInset);
}
return snackbarDependentView.onDependentViewChanged(parent, child);
}
return super.onDependentViewChanged(parent, child, dependency);
}
@Override
public boolean onStartNestedScroll(
final CoordinatorLayout coordinatorLayout,
final BottomNavigation child,
final View directTargetChild, final View target,
final int nestedScrollAxes) {
offset = 0;
if (!scrollable || !scrollEnabled) {
return false;
}
if ((nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0) {
log(
TAG, INFO, "isScrollContainer: %b, canScrollUp: %b, canScrollDown: %b", target.isScrollContainer(),
target.canScrollVertically(-1), target.canScrollVertically(1)
);
if (target.isScrollContainer() && (!target.canScrollVertically(-1) && !target.canScrollVertically(1))) {
return false;
}
}
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(final CoordinatorLayout coordinatorLayout, final BottomNavigation child, final View target) {
super.onStopNestedScroll(coordinatorLayout, child, target);
offset = 0;
}
@Override
public void onDirectionNestedPreScroll(
CoordinatorLayout coordinatorLayout,
BottomNavigation child,
View target, int dx, int dy, int[] consumed,
@ScrollDirection int scrollDirection) {
// stop nested scroll if target is not scrollable
// FIXME: not yet verified
if (target.isScrollContainer() && !target.canScrollVertically(scrollDirection)) {
log(TAG, WARN, "stopNestedScroll");
ViewCompat.stopNestedScroll(target);
}
offset += dy;
if (BottomNavigation.DEBUG) {
log(
TAG, INFO, "onDirectionNestedPreScroll(%d, %s, %b)", scrollDirection, target,
target.canScrollVertically(scrollDirection)
);
}
if (offset > scaledTouchSlop) {
handleDirection(coordinatorLayout, child, ScrollDirection.SCROLL_DIRECTION_UP);
offset = 0;
} else if (offset < -scaledTouchSlop) {
handleDirection(coordinatorLayout, child, ScrollDirection.SCROLL_DIRECTION_DOWN);
offset = 0;
}
}
@Override
protected boolean onNestedDirectionFling(
CoordinatorLayout coordinatorLayout, BottomNavigation child, View target, float velocityX, float velocityY,
@ScrollDirection int scrollDirection) {
log(TAG, INFO, "onNestedDirectionFling(%g, %d)", velocityY, scrollDirection);
if (Math.abs(velocityY) > 1000) {
handleDirection(coordinatorLayout, child, scrollDirection);
}
return true;
}
@Override
public void onNestedScroll(
final CoordinatorLayout coordinatorLayout, final BottomNavigation child, final View target, final int dxConsumed,
final int dyConsumed, final int dxUnconsumed,
final int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
@Override
public void onNestedPreScroll(
final CoordinatorLayout coordinatorLayout, final BottomNavigation child, final View target, final int dx, final int dy,
final int[] consumed) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
}
@Override
public void onNestedVerticalOverScroll(
CoordinatorLayout coordinatorLayout, BottomNavigation child, @ScrollDirection int direction, int currentOverScroll,
int totalOverScroll) {
}
private void handleDirection(final CoordinatorLayout coordinatorLayout, BottomNavigation child, int scrollDirection) {
if (!enabled || !scrollable || !scrollEnabled) {
return;
}
if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_DOWN && hidden) {
setExpanded(coordinatorLayout, child, true, true);
} else if (scrollDirection == ScrollDirection.SCROLL_DIRECTION_UP && !hidden) {
setExpanded(coordinatorLayout, child, false, true);
}
}
protected void setExpanded(
final CoordinatorLayout coordinatorLayout, final BottomNavigation child, boolean expanded, boolean animate) {
log(TAG, INFO, "setExpanded(%b)", expanded);
animateOffset(coordinatorLayout, child, expanded ? 0 : maxOffset, animate);
if (null != listener) {
listener.onExpandStatusChanged(expanded, animate);
}
}
private void animateOffset(
final CoordinatorLayout coordinatorLayout,
final BottomNavigation child,
final int offset,
final boolean animate) {
log(TAG, INFO, "animateOffset(%d)", offset);
hidden = offset != 0;
ensureOrCancelAnimator(coordinatorLayout, child);
if (animate) {
animator.translationY(offset).start();
} else {
child.setTranslationY(offset);
}
}
private void ensureOrCancelAnimator(final CoordinatorLayout coordinatorLayout, final BottomNavigation child) {
if (animator == null) {
animator = ViewCompat.animate(child);
animator.setDuration(animationDuration);
animator.setInterpolator(INTERPOLATOR);
} else {
animator.cancel();
}
}
public abstract static class DependentView<V extends View> {
protected final V child;
protected final MarginLayoutParams layoutParams;
protected final int bottomMargin;
protected int height;
protected final int bottomInset;
protected final float originalPosition;
DependentView(V child, final int height, final int bottomInset) {
this.child = child;
this.originalPosition = child.getTranslationY();
this.layoutParams = (MarginLayoutParams) child.getLayoutParams();
this.bottomMargin = layoutParams.bottomMargin;
this.height = height;
this.bottomInset = bottomInset;
}
protected void onDestroy() {
layoutParams.bottomMargin = bottomMargin;
child.setTranslationY(originalPosition);
child.requestLayout();
}
abstract boolean onDependentViewChanged(CoordinatorLayout parent, BottomNavigation navigation);
}
public static class GenericDependentView extends DependentView<View> {
private static final String TAG = BottomBehavior.TAG + "." + GenericDependentView.class.getSimpleName();
GenericDependentView(final View child, final int height, final int bottomInset) {
super(child, height, bottomInset);
}
@Override
protected boolean onDependentViewChanged(final CoordinatorLayout parent, final BottomNavigation navigation) {
return true;
}
}
private static class SnackBarDependentView extends DependentView<SnackbarLayout> {
private static final String TAG = BottomBehavior.TAG + "." + SnackBarDependentView.class.getSimpleName();
private int snackbarHeight = -1;
SnackBarDependentView(final SnackbarLayout child, final int height, final int bottomInset) {
super(child, height, bottomInset);
}
@Override
boolean onDependentViewChanged(final CoordinatorLayout parent, final BottomNavigation navigation) {
log(TAG, INFO, "onDependentViewChanged");
if (Build.VERSION.SDK_INT < 21) {
int index1 = parent.indexOfChild(child);
int index2 = parent.indexOfChild(navigation);
if (index1 > index2) {
log(TAG, WARN, "swapping children");
navigation.bringToFront();
}
}
if (snackbarHeight == -1) {
snackbarHeight = child.getHeight();
}
final float maxScroll = Math.max(0, navigation.getTranslationY() - bottomInset);
final int newBottomMargin = (int) (height - maxScroll);
if (layoutParams.bottomMargin != newBottomMargin) {
layoutParams.bottomMargin = newBottomMargin;
child.requestLayout();
return true;
}
return false;
}
@Override
protected void onDestroy() {
super.onDestroy();
//scrollEnabled = true;
}
}
public interface OnExpandStatusChangeListener {
void onExpandStatusChanged(boolean expanded, final boolean animate);
}
}