前言部分
CoordinatorLayout这个view出现已经很久了,今天看到知乎上的一个神奇联动的效果,类似的效果在很多应用上都有使用,以前虽然经常看到但是没有实际去查看如何实现的。这几天特意去查看资料学习其中的方式,但是由于能力有限和资料不是很全面只学到了一个皮毛。在此做一个记录,也算是这几天的努力的小总结吧。
说到CoordinatorLayout我们最先试用过的AppBarLayout和CollapsingToolbarLayout实现折叠头部。或者实现视差效果等等。。这里我都不做介绍了文章很多,实现的效果也很棒,最主是不需要我们自己子们处理逻辑,系统都帮我们实现好了。
我们虽然使用过,但是其实并没有去深入了解过实现的原理是什么,今天就通过实现一个简单的效果来学习一下自定义的Behavior。
实现的就是类似知乎等一些阅读类app等效果,如下入:
![](https://i-blog.csdnimg.cn/blog_migrate/1536b04603774c4bc5b4d4cf8ed81e65.gif)
事实上我们做这些工作是为了更好的实现view之间的协调联动,在没有CoordinatorLayout辅助view协调滑动之前,我们都是通过parent来对触摸事件进行分发,但这这样做就需要我们对事件的传递有很深刻的理解,因为事件的传递是从外由内的,但是事件消耗确实从内向外,parent响应事件后在传递给内部的子view,子view可以决定是否消耗事件,但是这也有一个问题,就是子view消耗事件后,其他的view都不会在收到事件的信息。所以协调多个子view联动就变得很麻烦。
下面一些详细的讲解可以去连接看看大佬的介绍,我也是读了文章开始自己写一个小 demo练手的。
上面文章中有个总结很棒,这里直接贴出来。
自定义 Behavior 的两种目的
我们可以按照两种目的来实现自己的 Behavior
,当然也可以两种都实现啦
- 某个 view 监听另一个 view 的状态变化,例如大小、位置、显示状态等
- 某个 view 监听
CoordinatorLayout
内的NestedScrollingChild
的接口实现类的滑动状态
实例编写
最终目标实现:
头部view、尾部view和列表绑定,当列表处于初始位置的时候,下拉列表实现头部view同时拉出,上拉列表的时候实现头部view隐藏。尾部view的规则是列表上拉时候隐藏,列表下拉的时候出现。
第一步实现如下图:
![](https://i-blog.csdnimg.cn/blog_migrate/61a47db3a9aec11fd3c03ca3ecedaba5.gif)
public class RecyclerTitleBehavior extends CoordinatorLayout.Behavior<RecyclerView> {
public RecyclerTitleBehavior() {
}
public RecyclerTitleBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
return dependency instanceof TextView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
//计算列表y坐标,最小为0
float y = dependency.getTranslationY();
if (y > dependency.getHeight()) {
y = dependency.getHeight();
} else if (y < 0) {
y = 0;
}
child.setY(y);
return true;
}
}
这个比较简单,主要是通过依赖头部view来改变自己的位置。如上面所说的两种方式一种:RecyclerView是负责监控头部view的别致变化。头部view发生位移变化时候,自身也作出相应的位移变化。
-
-
这一步实现头部view监听CoordinatorLayout的直接子 view的滑动,RecyclerView之所以作为子view可以被监听,因为其内部是实现了NestedScrollingChild2接口,所以当发生滑动的时候会通过NestedScrollingChildHelper调用父view的onStartNestedScroll接口(前提是父 view实现了NestedScrollingParent2接口),CoordinatorLayout作为父view可以接收到它滑动的信息,然后通过循环遍历childCount获取Behavior,通过Behavior来把滑动信息发送给所有的子view(这里父 view也可以自己选择接受滑动,这样会优先子 view滑动)。其他子view如果依赖这个view则实现其他子 view的滑动;
-
同时其他的子 view(不可滑动的View默认是没有实现NestedScrollingChild2接口)通过设置Behavior也实现了NestedScrollingChild2接口,通过Behavior来接收父view的滑动事件回调。如果子view发生滑动后,也会通过接口的形式回掉到NestedScrollingParent2接口中,然后父view决定自己是否配合子view的滑动,然后父view把自己滑动的情况反馈给子view,这时候子view再从新计算滑动的距离,在进行滑动。
-
如上所述,基本就是子view滑动,告诉一下父view我要滑动了,父view首先考虑一下自己要不要滑动,然后再告诉其它的子view有个子view滑动了,你们谁关注它了想一起动啊,然后把父 view自己滑动得结果告诉滑动的子view,让他重新计算一下距离,然后滑动剩下的距离。
public class TextBehavior extends CoordinatorLayout.Behavior<TextView> { /** * 上边界 */ private boolean isUpLimit = false; /** * 下边界 */ private boolean isDownLimit = false; public TextBehavior() { } public TextBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: isUpLimit = false; isDownLimit = false; default: break; } return super.onInterceptTouchEvent(parent, child, ev); } @Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); //dy 下 为 负数 && 上 为 正数 if (target instanceof RecyclerView) { RecyclerView recyclerView = (RecyclerView) target; LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); int firstPosition = layoutManager.findFirstCompletelyVisibleItemPosition(); float translationY = child.getTranslationY() - dy; // LogUtil.d("translationY===" + translationY); // child.setTranslationY(translationY); // if (firstPosition == 0 && translationY == 0) { // isUpLimit = true; // } // if (firstPosition == 0 && canScroll(dy)) { if (translationY > child.getHeight()) { translationY = child.getHeight(); isUpLimit = false; isDownLimit = true; } else if (translationY < 0) { translationY = 0; isUpLimit = true; isDownLimit = false; } child.setTranslationY(translationY); consumed[1] = dy; } } } private boolean canScroll(int dy) { if (isUpLimit && dy > 0) { return false; } if (isDownLimit && dy < 0) { return false; } return true; } }
基本上就这样,这也是醉简单的滑动效果了。
-
下面我们在添加一个联动的子view
我们添加一个尾部的View,它也监听另外的一个View(就是RecyclerView了),如果View滑动了一段距离,尾部的View也会有对应的动作。代码如下:
/**
* @Author : dongfang
* @Created Time : 2019-05-28 15:08
* @Description:
*/
public class TextBottomBehavior extends CoordinatorLayout.Behavior<View> {
/**
* 上边界
*/
private boolean isUpLimit = false;
/**
* 下边界
*/
private boolean isDownLimit = false;
public TextBottomBehavior() {
}
public TextBottomBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isUpLimit = false;
isDownLimit = false;
default:
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
//dy 下 为 负数 && 上 为 正数
if (target instanceof RecyclerView) {
float translationY = child.getTranslationY() + dy;
LogUtil.d("translationY===" + child.getTranslationY() + "finalY==" + translationY);
if (canScroll(dy)) {
if (translationY < -child.getHeight()) {
translationY = -child.getHeight();
isUpLimit = false;
isDownLimit = true;
} else if (translationY > 0) {
translationY = 0;
isUpLimit = true;
isDownLimit = false;
}
child.setTranslationY(translationY);
// consumed[1] = dy;
}
}
}
private boolean canScroll(int dy) {
if (isUpLimit && dy > 0) {
return false;
}
if (isDownLimit && dy < 0) {
return false;
}
return true;
}
}
这个和上个基本是一致的,只是运动的方向是反的。
还有一点要注意,我们的被监听的view只是跟随头部的view滑动,并不跟随尾部View滑动
结尾部分
鉴于能力有限,写的不是很清楚的话,希望大家指出,如果有更好的资料也请给我留言。
final 谢谢