在阅读类app的文章页面中我们通常会见到这样的效果:
自从有了CoordinatorLayout之后,直接用自定义的Behavior就可以实现了。
首先自定义一个通用的VerticalScrollingBehavior来判断垂直滚动方向:
/** * Created by Nikola on 11/22/2015. */ 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; 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 * @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); } }
再自定义一个继承VerticalScrollingBehavior的BottomNavigationBehavior来处理底部导航的隐藏和显示动画
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.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 boolean isTablet; private boolean hidden = false; private ViewPropertyAnimatorCompat mOffsetValueAnimator; private int mSnackbarHeight = -1; private boolean scrollingEnabled = true; private boolean hideAlongSnackbar = false; int[] attrsArray = new int[] { android.R.attr.id }; public BottomNavigationBehavior() { super(); } public BottomNavigationBehavior(Context context, AttributeSet attrs) { super(context, attrs); } 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 (!isTablet && 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); return layoutChild; } @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(V child) { if (mOffsetValueAnimator == null) { mOffsetValueAnimator = ViewCompat.animate(child); mOffsetValueAnimator.setDuration(200); mOffsetValueAnimator.setInterpolator(INTERPOLATOR); } else { mOffsetValueAnimator.cancel(); } } 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 (!isTablet && 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 (!isTablet && dependency instanceof Snackbar.SnackbarLayout) { if (mSnackbarHeight == -1) { mSnackbarHeight = dependency.getHeight(); } int targetPadding = (mSnackbarHeight + child.getMeasuredHeight()); dependency.setPadding(dependency.getPaddingLeft(), dependency.getPaddingTop(), dependency.getPaddingRight(), targetPadding ); } } } }
如果绝对隐藏的速度太快,可以修改ensureOrCancelAnimator方法中的setDuration的值,这里是200。
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/coordinatorlayout" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/text_favorites" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/long_text" /> <TextView android:id="@+id/text_schedules" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/text_schedules" android:visibility="gone" /> <TextView android:id="@+id/text_music" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/text_music" android:visibility="gone" /> </RelativeLayout> </android.support.v4.widget.NestedScrollView> <android.support.design.widget.BottomNavigationView android:id="@+id/bottom_navigation" android:layout_gravity="bottom" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" app:itemBackground="@color/colorPrimary" app:itemIconTint="@color/white" app:itemTextColor="@color/white" app:layout_behavior=".BottomNavigationBehavior" app:menu="@menu/bottom_navigation_main" /> </android.support.design.widget.CoordinatorLayout>
其中app:layout_behavior=".BottomNavigationBehavior"为BottomNavigationBehavior类的位置,如果不在根目录还要加上包名。
类似
app:layout_behavior="com.xxx.behavior.BottomNavigationBehavior"
这里的BottomNavigationView可以是任意的view和布局,只要能保证是在页面的最底部就可以了。
在前面的动态图中,只有底部导航隐藏了,Toolbar并没有同时隐藏,其实这种需求也是很常见的,不过Toolbar的隐藏官方直接用app:layout_scrollFlags="scroll|enterAlways|snap"解决了。
<android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" app:elevation="0dp" > <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="@dimen/bar_height" android:background="@color/colorPrimary" app:contentInsetLeft="0dp" app:contentInsetStart="0dp" android:theme="@style/AppTheme.AppBarOverlay" app:layout_scrollFlags="scroll|enterAlways|snap" /> </android.support.design.widget.AppBarLayout>
本文的代码来自Bottom Navigation Behavior,对实现做了修改,删除了部分貌似无用的代码。