使用CoordinatorLayout打造各种炫酷的效果
自定义Behavior —— 仿知乎,FloatActionButton隐藏与展示
前段时间写了一篇博客使用CoordinatorLayout打造各种炫酷的效果 ,主要介绍了APPBarLayout和CollapsingToolbarLayout的基本用法,AppBarLayout主要用来实现在滚动的时候ToolBar的 隐藏于展示,CollapsingToolbarLayout主要用来实现parallax和pin等效果。如果你对CoordinatorLayout还不了解的话,请先阅读这篇文章。
写作思路
CoordinatorLayout Behavior 简介 怎样自定义 Behavior 仿知乎效果 自定义 Behavior 的实现 自定义 Behavior 两种方法的 对比 FloatActionButton 自定义 Behavior 效果的实现 题外话 今天就来讲解怎样通过自定义behavior来实现各种炫酷的效果 ,效果图如下
下面让我们一起来看一下怎样实现仿知乎的效果 老规矩,先看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 <?xml version="1.0" encoding="utf-8" ?> <android.support.design.widget.CoordinatorLayout android:id="@+id/coordinatorLayout" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <android.support.design.widget.AppBarLayout android:id="@+id/index_app_bar" theme="@style/AppTheme.AppBarOverlay" android:layout_width="match_parent" android:layout_height="wrap_content" > <RelativeLayout android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary" app:layout_scrollFlags="scroll|enterAlways" > <ImageView android:id="@+id/search" android:layout_width="24dp" android:layout_height="24dp" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:src="@drawable/search" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:layout_toRightOf="@id/search" android:text="搜索话题、问题或人" android:textSize="16sp" /> </RelativeLayout> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" > </android.support.v7.widget.RecyclerView> <!--使用RadioGroup来实现tab的切换--> <RadioGroup android:id="@+id/rg" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="@color/bg_tab" android:orientation="horizontal" app:layout_behavior="@string/behavior_footer" > <RadioButton android:id="@+id/rb_home" style="@style/bottom_tab" android:drawableTop="@drawable/sel_home" android:text="Home" /> <RadioButton android:id="@+id/rb_course" style="@style/bottom_tab" android:drawableTop="@drawable/sel_course" android:text="course" /> <RadioButton android:id="@+id/rb_direct_seeding" style="@style/bottom_tab" android:drawableTop="@drawable/sel_direct_seeding" android:text="direct" /> <RadioButton android:id="@+id/rb_me" style="@style/bottom_tab" android:drawableTop="@drawable/sel_me" android:text="me" /> </RadioGroup> </android.support.design.widget.CoordinatorLayout>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <style name="bottom_tab"> <item name="android:layout_width">0dp</item> <item name="android:layout_height">60dp</item> <item name="android:layout_weight">1</item> <item name="android:text">0dp</item> <item name="android:gravity">center</item> <item name="android:textColor">@drawable/sel_bottom_tab_text</item> <item name="android:padding">10dp</item> <item name="android:button">@null</item> </style> <style name="bottom_tab"> <item name="android:layout_width">0dp</item> <item name="android:layout_height">60dp</item> <item name="android:layout_weight">1</item> <item name="android:text">0dp</item> <item name="android:gravity">center</item> <item name="android:textColor">@drawable/sel_bottom_tab_text</item> <item name="android:padding">10dp</item> <item name="android:button">@null</item> </style>
思路分析 根据动态如可以看到,主要有两个效果
上面的AppBarLayout 向上滑动的时候会隐藏,向下滑动的时候会展示,说白了就是给APPLayout的子View Relativelayout 设置 app:layout_scrollFlags=”scroll|enterAlways”,核心代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <android.support.design.widget.AppBarLayout android:id="@+id/index_app_bar" theme="@style/AppTheme.AppBarOverlay" android:layout_width="match_parent" android:layout_height="wrap_content"> <RelativeLayout android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary" app:layout_scrollFlags="scroll|enterAlways"> ---- </RelativeLayout> </android.support.design.widget.AppBarLayout>
下面的 RadioGroup ,我们可以看到,向上 滑动的时候会隐藏,向下滑动的时候会显示,其实我们只是给其设置了 behavior 而已 app:layout_behavior=”@string/behavior_footer”,那这个behavior_footer是什么东西,别急 ,下面就是介绍了 1 <string name="behavior_footer">com.xujun.contralayout.behavior.FooterBehavior</string>
Behavior简介
Behavior是CoordinatorLayout里面的一个内部类,通过它我们可以与 CoordinatorLayout的一个或者多个子View进行交互,包括 drag,swipes, flings等手势动作。
今天 我们主要着重介绍里面的几个方法
方法 解释 boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) 确定child View 是否有一个特定的兄弟View作为布局的依赖(即dependency) boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) 当child View 的 dependent view 发生变化的时候,这个方法会调用 boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) 当CoordinatorLayout 的直接或者非直接子View开始准备嵌套滑动的时候会调用 void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) 当嵌套滑动的 时候,target尝试滑动或者正在滑动的 时候会调用
关于更多方法,请参考官网文档说明
怎样自定义Behavior 前面已经说到,今天主要介绍四个方法,这里我们把它分为两组。
第一组
1 2 3 4 5 // 决定child 依赖于把一个 dependency boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) // 当 dependency View 改变的时候 child 要做出怎样的响应 boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency)
第二组
1 2 3 4 5 // 当CoordinatorLayout的直接或者非直接子View开始嵌套滑动的时候,会调用这个方法 boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child, View directTargetChild, View target, int nestedScrollAxes) // 当嵌套滑动的时候,target 尝试滑动或者正在滑动会调用这个方法 onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
首先我们先看第一组是怎样实现的? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class FooterBehaviorDependAppBar extends CoordinatorLayout .Behavior <View > { public static final String TAG = "xujun" ; public FooterBehaviorDependAppBar (Context context, AttributeSet attrs) { super (context, attrs); } @Override public boolean layoutDependsOn (CoordinatorLayout parent, View child, View dependency) { return dependency instanceof AppBarLayout; } @Override public boolean onDependentViewChanged (CoordinatorLayout parent, View child, View dependency) { float translationY = Math.abs(dependency.getTop()); child.setTranslationY(translationY); Log.i(TAG, "onDependentViewChanged: " + translationY); return true ; } }
思路分析
这里我们要分清两个概念,child 和 dependency ,child 是我们要改变的坐标的view,而 dependency 是child 的 附属 ,即child 会随着 dependency 坐标的改变而改变。
比如上面的例子:当我们把 app:layout_behavior=”com.xujun.contralayout.behavior.FooterBehaviorDependAppBar” 设置给 RadioGroup 的时候,这时候 child 就是 RadioGroup ,而 dependency 就是 APPBarLayout ,因为我们在 layoutDependsOn 方法里面 ,返回 dependency instanceof AppBarLayout ,即当 dependency 是 AppBarLayout 或者 AppBarLayout的子类的时候返回TRUE。
1 2 3 4 5 //当 dependency instanceof AppBarLayout 返回TRUE,将会调用onDependentViewChanged()方法 @Override public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) { return dependency instanceof AppBarLayout; }
而之所以 RadioGroup 在向上滑动的时候会隐藏,向下滑动的时候会显示,是因为我们在 onDependentViewChanged 方法的时候 动态地根据 dependency 的 top 值改变 RadioGroup 的 translationY 值,核心 代码如下
1 2 3 4 5 6 7 8 9 @Override public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) { //根据dependency top值的变化改变 child 的 translationY float translationY = Math.abs(dependency.getTop()); child.setTranslationY(translationY); Log.i(TAG, "onDependentViewChanged: " + translationY); return true; }
到此第一种思路分析为止
第二种思路 主要是根据 onStartNestedScroll() 和 onNestedPreScroll()方法 来实现的,
当我们开始滑动的时候,我们判断是否是垂直滑动,如果是返回TRUE,否则返回 FALSE,返回TRUE,会接着调用onNestedPreScroll()等一系列方法。 1 2 3 4 5 6 //1.判断滑动的方向 我们需要垂直滑动 @Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; }
在 onNestedPreScroll() 方法里面,我们根据我们的逻辑来决定是否显示 target , 在这里我们是向上上滑动的时候,如果我们滑动的距离超过 target 的高度 并且 当前是可见的状态下,我们执行动画,隐藏 target,当我们向下滑动的时候,并且 View 是不可见的情况下,我们执行动画 ,显示target 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //2.根据滑动的距离显示和隐藏footer view @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) { if (dy > 0 && sinceDirectionChange < 0 || dy < 0 && sinceDirectionChange > 0) { child.animate().cancel(); sinceDirectionChange = 0; } sinceDirectionChange += dy; int visibility = child.getVisibility(); if (sinceDirectionChange > child.getHeight() && visibility == View.VISIBLE) { hide(child); } else { if (sinceDirectionChange < 0 && (visibility == View.GONE || visibility == View .INVISIBLE)) { show(child); } } }
全部代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 public class FooterBehavior extends CoordinatorLayout .Behavior <View > { private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); private int sinceDirectionChange; public FooterBehavior (Context context, AttributeSet attrs) { super (context, attrs); } @Override public boolean onStartNestedScroll (CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) { return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 ; } @Override public void onNestedPreScroll (CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int [] consumed) { if (dy > 0 && sinceDirectionChange < 0 || dy < 0 && sinceDirectionChange > 0 ) { child.animate().cancel(); sinceDirectionChange = 0 ; } sinceDirectionChange += dy; int visibility = child.getVisibility(); if (sinceDirectionChange > child.getHeight() && visibility == View.VISIBLE) { hide(child); } else { if (sinceDirectionChange < 0 && (visibility == View.GONE || visibility == View .INVISIBLE)) { show(child); } } } private void hide (final View view) { ViewPropertyAnimator animator = view.animate().translationY(view.getHeight()). setInterpolator(INTERPOLATOR).setDuration(200 ); animator.setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart (Animator animator) { } @Override public void onAnimationEnd (Animator animator) { view.setVisibility(View.GONE); } @Override public void onAnimationCancel (Animator animator) { show(view); } @Override public void onAnimationRepeat (Animator animator) { } }); animator.start(); } private void show (final View view) { ViewPropertyAnimator animator = view.animate().translationY(0 ). setInterpolator(INTERPOLATOR). setDuration(200 ); animator.setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart (Animator animator) { } @Override public void onAnimationEnd (Animator animator) { view.setVisibility(View.VISIBLE); } @Override public void onAnimationCancel (Animator animator) { hide(view); } @Override public void onAnimationRepeat (Animator animator) { } }); animator.start(); } }
两种实现方法的对比和总结 我们知道第一种方法我们主要是重写layoutDependsOn 和 onDependentViewChanged 这两个方法,这个方法在 layoutDependsOn 判断 dependency 是否是 APpBarLayout 的实现类,所以 会导致 child 依赖于 AppBarLayout,灵活性不是太强
而第二种方法,我们主要是重写 onStartNestedScroll 和 onNestedPreScroll 这两个方法,判断是否是垂直滑动,是的话就进行处理,灵活性大大增强,推荐使用这一种方法
需要注意的是不管是第一种方法,还是第二种方法,我们都需要重写带两个构造方法的函数,因为底层机制会采用反射的形式获得该对象
1 2 3 public FooterBehavior(Context context, AttributeSet attrs) { super(context, attrs); }
效果图如下
缩放隐藏的
向上向下隐藏的
布局代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout android:id="@+id/activity_floating_action_button" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" tools:context="com.xujun.contralayout.UI.FloatingActionButtonActivity"> <android.support.design.widget.AppBarLayout android:id="@+id/index_app_bar" theme="@style/AppTheme.AppBarOverlay" android:layout_width="match_parent" android:layout_height="wrap_content"> <RelativeLayout android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorPrimary" app:layout_scrollFlags="scroll|enterAlways"> <ImageView android:id="@+id/search" android:layout_width="24dp" android:layout_height="24dp" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:src="@drawable/search"/> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="10dp" android:layout_toRightOf="@id/search" android:text="搜索话题、问题或人" android:textSize="16sp"/> </RelativeLayout> </android.support.design.widget.AppBarLayout> <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> </android.support.v7.widget.RecyclerView> <android.support.design.widget.FloatingActionButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|right|end" android:layout_marginBottom="40dp" android:layout_marginRight="25dp" android:background="@android:color/holo_green_light" android:src="@drawable/add" app:layout_behavior="@string/behavior_my_fab_scale"/> </android.support.design.widget.CoordinatorLayout>
如果想使用不同的效果,只需要给 FloatingActionButton 制定不同的 bevaior 即可
1 app:layout_behavior="com.xujun.contralayout.behavior.MyFabBehavior"
自定义behavior 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 /** * FloatingActionButton behavior 向上向下隐藏的 * @author xujun on 2016/12/1. * @email gdutxiaoxu@163.com */ public class MyFabBehavior extends CoordinatorLayout.Behavior<View> { private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); private float viewY;//控件距离coordinatorLayout底部距离 private boolean isAnimate;//动画是否在进行 public MyFabBehavior(Context context, AttributeSet attrs) { super(context, attrs); } //在嵌套滑动开始前回调 @Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) { if(child.getVisibility() == View.VISIBLE&&viewY==0){ //获取控件距离父布局(coordinatorLayout)底部距离 viewY=coordinatorLayout.getHeight()-child.getY(); } return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//判断是否竖直滚动 } //在嵌套滑动进行时,对象消费滚动距离前回调 @Override public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) { //dy大于0是向上滚动 小于0是向下滚动 if (dy >=0&&!isAnimate&&child.getVisibility()==View.VISIBLE) { hide(child); } else if (dy <0&&!isAnimate&&child.getVisibility()==View.GONE) { show(child); } } //隐藏时的动画 private void hide(final View view) { ViewPropertyAnimator animator = view.animate().translationY(viewY).setInterpolator(INTERPOLATOR).setDuration(200); animator.setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { isAnimate=true; } @Override public void onAnimationEnd(Animator animator) { view.setVisibility(View.GONE); isAnimate=false; } @Override public void onAnimationCancel(Animator animator) { show(view); } @Override public void onAnimationRepeat(Animator animator) { } }); animator.start(); } //显示时的动画 private void show(final View view) { ViewPropertyAnimator animator = view.animate().translationY(0).setInterpolator(INTERPOLATOR).setDuration(200); animator.setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { view.setVisibility(View.VISIBLE); isAnimate=true; } @Override public void onAnimationEnd(Animator animator) { isAnimate=false; } @Override public void onAnimationCancel(Animator animator) { hide(view); } @Override public void onAnimationRepeat(Animator animator) { } }); animator.start(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 /** * <p>下拉时显示FAB,上拉隐藏,留出更多位置给用户。</p> * Created on 2016/12/1. * * @author xujun */ public class ScaleDownShowBehavior extends FloatingActionButton.Behavior { /** * 退出动画是否正在执行。 */ private boolean isAnimatingOut = false; private OnStateChangedListener mOnStateChangedListener; public ScaleDownShowBehavior(Context context, AttributeSet attrs) { super(); } @Override public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View directTargetChild, View target, int nestedScrollAxes) { return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL; } @Override 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) {//往下滑 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); } } } public void setOnStateChangedListener(OnStateChangedListener mOnStateChangedListener) { this.mOnStateChangedListener = mOnStateChangedListener; } // 外部监听显示和隐藏。 public interface OnStateChangedListener { void onChanged(boolean isShow); } public static <V extends View> ScaleDownShowBehavior from(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 ScaleDownShowBehavior)) { throw new IllegalArgumentException("The view is not associated with ScaleDownShowBehavior"); } return (ScaleDownShowBehavior) behavior; } private ViewPropertyAnimatorListener viewPropertyAnimatorListener = 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; } }; }
思路这里就不详细展开了,因为前面在讲解 仿知乎效果的时候已经讲过了,大概就是根据不同的滑动行为执行不同的动画 而已
题外话 通过这篇博客,熟悉 CoordinatorLayout 的 各种用法,同时也初步理解了自定义Behavior的思路 同时复习了动画的相关知识 如果你觉得效果还不错,欢迎到我的github上面star,github地址 文章首发地址CSDN: http://blog.csdn.net/gdutxiaoxu/article/details/53453958
源码下载地址: https://github.com/gdutxiaoxu/CoordinatorLayoutExample.git