本博客实现效果:
今天效果的源代码下载链接在文章末尾。
实现分析
这个效果其实并不难实现,但是它的用处很大,当用户手指上滑,屏幕上显示下方内容的时候,隐藏Toolbar、Tab导航、FAB来腾出更大的空间显示内容,让用户爽。简单粗暴,但这就是我们的目的。
首先就是头部的Toolbar,这个就不用说了吧,基本会,不会的人随便看我一篇博客的demo都有这个效果,简直小学级别。
其次来看看FAB(FlocationActionButton)的显示和隐藏,知乎是用的平移,我们这里做个优化改动,当然平移也是可以的,如果你看过我的Material Design系列,自定义Behavior之上滑显示返回顶部按钮这篇博客的话。那么我们的FAB的动画隐藏和显示也是用上一篇博客的原理,没有看上一篇博客的同学需要回过头看看噢,这里不在赘述。
最后来看下面的Tab导航的隐藏和显示,这个确确实实用平移更好是吧,然而相信你如果看过我Material Design系列,Behavior之BottomSheetBehavior与BottomSheetDialog这篇博客的话,这个效果实现起来也不难。强烈建议看下文之前读这篇文章,不然真的没法继续看下去了。
其实代码量还是很少的,主要是Behavior原理、Behavior和CoordinatorLayout如何结合使用。so,强烈建议去上读下上面两篇博客噢。
……
好的,五分钟过去了,我相信你大概已经速读了上面提到的两篇博客了。那么在第一篇FAB的那篇博客中实现的效果是手指向上滑时(屏幕显示下方的内容时)显示FAB用来回到顶部,但是这里刚好是相反的:向上滑时隐藏FAB。如果你认真读了原理解释的那一段或者运行过demo,这个效果so easy吧。第二篇博客中也讲到了如何隐藏和显示这个Tab导航,那么有的同学就觉得今天的博客就结束了吧?答案当然是No了,不然我也不会再开一篇博客来讲这个了。
为什么呢?还是有难点的,难点在哪里?就是上面讲到的两个Behavior如何和CoordinatorLayout结合使用,同时实现两个效果。而且BottomSheetBehavior隐藏和显示Tab导航这个里面之前我们使用Button来控制的,如何做到`CoordinatorLayout中的ContentView滑动时动态的显示和隐藏Tab导航呢?
接下来来点真材实料,带领大家一起代码撸起来。
页面布局
上面的引文和介绍,我们已经知道了FAB的显示和隐藏用自定义Behavior实现,Tab导航用BottomSheetBehavior来实现,那么我们布局文件也该问世了:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:theme="@style/AppTheme.AppBarOverlay"><android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_scrollFlags="scroll|enterAlways|snap" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerViewandroid:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="match_parent"app:layout_behavior="@string/appbar_scrolling_view_behavior" /> <LinearLayoutandroid:id="@+id/tab_layout"android:layout_width="match_parent"android:layout_height="?actionBarSize"android:layout_alignParentBottom="true"android:background="@android:color/white"app:layout_behavior="@string/bottom_sheet_behavior"><Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="第一" /><Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="第二" /><Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="第三" /><Button android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="第四" /> </LinearLayout> <android.support.design.widget.FloatingActionButtonandroid:id="@+id/fab"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="bottom|end"android:layout_marginBottom="70dp"android:layout_marginEnd="16dp"android:layout_marginRight="16dp"android:src="@mipmap/ic_action_new"app:layout_behavior="@string/scale_down_show_behavior"app:layout_scrollFlags="scroll|enterAlways|snap" /></android.support.design.widget.CoordinatorLayout>还是稍微解释下,内容区域是一个RecyclerView,使用的Behavior是design的ScrollingViewBehavior:
public class ScaleDownShowBehavior extends FloatingActionButton.Behavior { public ScaleDownShowBehavior(Context context, AttributeSet attrs) {super(); }}这里我们的滑动方向还是不变,监听竖着方向的滑动:
@Overridepublic boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, ...) { return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;}那么我们就要稍微改一下我们的开始滑动时回调这个方法了:onNestedScroll():
@Override// 隐藏动画是否正在执行private boolean isAnimatingOut = false;public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if ((dyConsumed > 0 || dyUnconsumed > 0) && !isAnimatingOut && child.getVisibility() == View.VISIBLE) {// 手指上滑,隐藏FABAnimatorUtil.scaleHide(child, listener); } else if ((dyConsumed < 0 || dyUnconsumed < 0) && child.getVisibility() != View.VISIBLE) {AnimatorUtil.scaleShow(child, null);// 手指下滑,显示FAB }}private ViewPropertyAnimatorListener listener = new ViewPropertyAnimatorListener() { @Override public void onAnimationStart(View view) {isAnimatingOut = true; } @Override public void onAnimationEnd(View view) {isAnimatingOut = false;view.setVisibility(View.GONE); } @Override public void onAnimationCancel(View arg0) {isAnimatingOut = false; }};好吧,代码非常少,完成了。那么我们就在string.xml中定义好,刚才我们引用的变量@string/scale_down_show_behavior:
// 外部监听显示和隐藏。public interface OnStateChangedListener { void onChanged(boolean isShow);}然后在ScaleDownShowBehavior的onNestedScroll()方法中回调:
private OnStateChangedListener mOnStateChangedListener;public void setOnStateChangedListener(OnStateChangedListener mOnStateChangedListener) { this.mOnStateChangedListener = mOnStateChangedListener;}@Overridepublic void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) { if ((dyConsumed > 0 || dyUnconsumed > 0) && !isAnimatingOut && child.getVisibility() == View.VISIBLE) {//往下滑AnimatorUtil.scaleHide(child, viewPropertyAnimatorListener);if (mOnStateChangedListener != null) { mOnStateChangedListener.onChanged(false);} } else if ((dyConsumed < 0 || dyUnconsumed < 0) && child.getVisibility() != View.VISIBLE) {AnimatorUtil.scaleShow(child, null);if (mOnStateChangedListener != null) { mOnStateChangedListener.onChanged(true);} }}好完美啊。来来来,设置一个监听。。。我勒个去,突然发现怎么从FAB拿到这个Behavior啊?且看我下面的分析,保证让你柳暗花明又一村啊。
public static <V extends View> ScaleDownShowBehavior from(V view) { ViewGroup.LayoutParams params = view.getLayoutParams(); if (!(params instanceof CoordinatorLayout.LayoutParams)) {throw new IllegalArgumentException("这个View不是CoordinatorLayout的子View"); } CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior(); if (!(behavior instanceof ScaleDownShowBehavior)) {throw new IllegalArgumentException("这个View的Behaviro不是ScaleDownShowBehavior"); } return (ScaleDownShowBehavior) behavior;}所以我们在Activity中:
private BottomSheetBehavior mBottomSheetBehavior;@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.zhihu_main); ScaleDownShowBehavior scaleDownShowFab = ScaleDownShowBehavior.from(FAB); scaleDownShowFab.setOnStateChangedListener(onStateChangedListener); mBottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.tab_layout));}private OnStateChangedListener onStateChangedListener = new OnStateChangedListener() { @Override public void onChanged(boolean isShow) {mBottomSheetBehavior.setState(isShow ? BottomSheetBehavior.STATE_EXPANDED: BottomSheetBehavior.STATE_COLLAPSED); }};哎哟喂,不知不觉中已经把我们的效果实现了,这里最重要的就是onStateChangedListener了,这里实现了Tab导航的隐藏和显示,它的状态是从ScaleDownShowBehavior中回调出来的。
private boolean initialize = false;@Overridepublic void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (!initialize) {initialize = true;mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); }}源码下载:http://xiazai.jb51.net/201609/yuanma/AndroidBehavior(jb51.net).rar