实现 RecyclerView 嵌套平滑滚动
列表嵌套滑动,关键点在于如何分派事件给父列表还是子列表。
具体节点如下:
1.当父类可以向下滑动时,事件应该交给父类。
2.当父类不能再向下滑动时,事件再交给子类。
3.向上滑动时,如果滑动区域是在子类上,事件首先交给子类,如不在,事件则交给父类。
4.向上滑动时,如子类已经滑动到顶,则事件交给父类。
这里为了方便父子类事件交替,父子间要相互持有引用。为了简化引用赋值,我的思路是在子类初始化时,让其自己去寻找嵌套的父类,并完成父类对子类的引用赋值。
//继承 RecyclerView 在构造方法中调用
postDelayed(new Runnable(){
@Override
public void run(){
if(isNestedScrollingEnabled()) {
ViewParent p = getParent();
while(p != null) {
//这里不要求父子要相邻嵌套,可以隔个几层,但最好还是相邻。
//思路就是沿着视图树一直往上找。
if(p instanceof ParentRecyclerView) {
mParentView = (ParentRecyclerView)p;
mParentView.setNestedChildView(ChildRecyclerView.this);
}
p = p.getParent();
}
}
}
}, 50);
针对上述4个节点的具体实现思路
1.在父类消耗事件时要时刻留意能否继续向下滑动,如不能就要及时让子类去滑动。
@Override
public boolean onTouchEvent(MotionEvent e) {
if(lastY = 0) {
lastY = e.getY();
}
//不能再向下滑动的话,就交给子类
if(!canScrollVertically(1)) {
if(childView != null) {
//自定义标记位,标记能否滑动
canScrollVertical = false;
childView.scrollBy(0, (int)(lastY - e.getY()));
}
}
lastY = e.getY();
return super.onTouchEvent(e);
}
此外,因为每次滑动都会涉及到事件结束,也就是 ACTION_UP 操作,需要在 up 时更新下是否能滑动的标记位。
@Override
public boolean onTouchEvent(MotionEvent e) {
if(lastY = 0) {
lastY = e.getY();
}
//省略......
if(MotionEvent.ACTION_UP == e.getAction()) {
//判断子类是否可以滑动
canScrollVertical = !getNestedChildViewCanScroll();
}
return super.onTouchEvent(e);
}
2.父类不能再向下滑动后,事件自然就交给子类处理了,这个由系统或有更上层的父类完成分发。子类要做的就是事件消费,子类可以上下滑动。
3,4.当子类上滑并且到顶部的时候,如果再滑动子类则应把事件交给父类继续执行。这么做才能保证滑动的连贯性,否则子类上滑到顶后,只有松手再滑才能让父类接着滑动。
@Override
public boolean onTouchEvent(MotionEvent e) {
//不能再向上滑动时,要交出事件
if(!canScrollVertically(-1)) {
if(parentView != null) {
//更新父类滑动标记
parentView.setCanScrollVertically(true);
//让父类拦截事件
parentView.requestDisallowInterceptTouceEvent(false);
//让父类拦截事件时,页面会有一点卡顿,这么做是为了修正一下滑动位移,试了一下就不卡了
//之前一直在尝试解决如何让事件柔和的交到父类上而不产生卡顿,后来借用节点1的思路尝试,效果还不错
parentView.scrollBy(0, -8);
}
}
}
另外还有一点要注意,如何判断父类能否滑动呢?光靠自身的 canScrollVertically() 是不够的,因为涉及到嵌套,所以要考虑子类,具体的判断依据是
canScrollVertical || childView == null || !childView.canScrollVertically(-1)
在 RecyclerView 消费事件时,最终能否垂直滑动是由 LayoutManager 来决定的,所以要重写 LayoutManager 的 canScrollVerically() 方法,用上面的值去替代。
至此,嵌套滑动的事情我们基本就解决了,接下去要考虑 Fling 效果,这块建议参考
仿京东、淘宝首页,通过两层嵌套的RecyclerView实现tab的吸顶效果
我试了效果还可以,具体原理有待后面分析。
有了滑动和惯性滑动后,RecyclerView 嵌套基本也就可以了,我之所以需要自己去实现,一方面想了解原理,另一方面布局不太合适。因为我的父类 RecyclerView 外使用的是 SmartRefreshLayout 和参考资料里的不太兼容,如果按参考资料里的来,改动又比较大,于是我就做了拆解改造。
问题:因为外层包的是 SmartRefreshLayout,当出现节点 4 的情况时,会出现父类没有上滑,而是触发了下拉刷新。
解决方法:
smartRL.setScrollBoundaryDecider(new ScrollBoundaryDecider(){
@Override
public boolean canRefresh(View content) {
return !recyclerView.canScrollVertically(-1);
}
//省略......
});
如果没用 SmartRefreshLayout,换成别的,我想应该也是可以的。
参考文章:
NestedRecyclerView
仿京东、淘宝首页,通过两层嵌套的RecyclerView实现tab的吸顶效果
关于View中mParent的来龙去脉
Android 仿京东,淘宝RecyclerView嵌套ViewPager嵌套RecyclerView商品展示