淘宝首页Bug!嵌套滑动及NestedScroll
刚复习完View事件分发、滑动冲突--《Android开发艺术探索》阅读笔记——第三章part2,接着想起前段时间项目中首页重构,遇到的嵌套滑动问题,以及CoordinatorLayout 和 AppbarLayout 联动原理。去学习了下相关知识。在此记录一下,备忘~
学习嵌套滑动的相关文章(点击“阅读原文”查看):
自定义View事件之进阶篇(一)-NestedScrolling(嵌套滑动)机制.
Android NestedScrolling机制完全解析 带你玩转嵌套滑动
一、项目实例--电商首页
1、嵌套滑动的问题点
看懂了以上文章后,现在来分享一下项目中的问题。因为公司项目同为电商,也恰好看了淘宝、京东的首页,就拿它俩举例吧。
通常首页都是一个RecyclerView,然后底部是Tab+frangment(内部recyclerview)组成的瀑布流商品---- 一起作为外部RecyclerView的最后一个item,很多电商都是这样。分别看下淘宝、京东的 外部RecyclerView(整个首页列表)、内部RecyclerView(底部tab中的商品流列表) 嵌套时的滑动效果。
可以清楚看到:
京东:滑动很顺畅,没有停滞的情况,tab到顶部后就 紧接着 滑动内部商品列表了。整个过程手指是连续拖动的,没有抬起。
淘宝:在tab滑到顶部后,手指继续拖动,但商品流是不能滑动的。这时手指抬起然后再次拖动商品流 才会滑动。
很显然,我们认为京东的滑动更丝滑。那为啥淘宝会出现这个情况呢?
2、缺陷原因分析
原因分析:从view事件分发机制 我们知道,当parent View拦截事件后,那同一事件序列的事件会直接都给parent处理,子view不会接受事件了。所以 按照正常处理滑动冲突的思路处理----当tab没到顶部时,parent拦截事件,tab到顶部时 parent就不拦截事件,但是由于手指没抬起来,所以这一事件序列还是继续给parent,不会到内部RecyclerView,所以商品流就不会滑动了。(这里不清楚的可以参考View事件分发、滑动冲突--《Android开发艺术探索》阅读笔记——第三章part2)
解决方案:使用嵌套滑动,具体如下。
1、添加嵌套滑动父布局
<*.NestedScrollLayout2
android:id="@+id/nest_scroll_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/orv_main_page"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</NestedScrollLayout2>
在 外部RecyclerView加上了父布局,NestedScrollLayout2,NestedScrollLayout2是实现==NestedScrollingParent2==接口的,这个很重要。
2、嵌套滑动父布局的实现原理
NestedScrollLayout2 extends FrameLayout implements NestedScrollingParent2
上面说了,实现==NestedScrollingParent2==接口很重要,目的就是 在 开始滑动 外部RecyclerView 时、开始滑动内部RecyclerView时,都询问NestedScrollLayout2是否处理且如何处理。
所以,根据我们的问题,在向上滑动内部RecyclerView时,如果tab没到顶就让parent消费事件,且滑动外部RecyclerView;到顶了,就滑内部RecyclerView。相对的, 向下滑动内部RecyclerView时,如果还能滑就滑内部RecyclerView;如果已经滑到顶部就让parent去滑动外部RecyclerView。
在滑外部外部RecyclerView时,也是一样逻辑。 具体看代码,这里贴NestedScrollLayout2关键代码,有注释说明,就是对上面文字的代码实现而已。其中mRootList是外部RecyclerView,mChildList是内部RecyclerView,childTop是tab这个view的top 用于判断是否到顶部。scrollListener是监听tab到顶部后设置其背景色用的。主要关注调用scrollBy时滚动的是哪个列表,滚动了多少。
(如果阅读这段理解不清晰,建议再去看上面提的嵌套滑动文章)
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
int type) {
if (mChildView != null) {
//target就是接受到事件的child,这里就是parent接受嵌套滑动后的pre处理
if (target == mRootList) {
//滑外部列表
onParentScrolling(mChildView.getTop(), dy, consumed);
} else {
//滑内部列表
onChildScrolling(mChildView.getTop(), dy, consumed);
}
}
}
/**
* 父列表在滑动
*/
private void onParentScrolling(int childTop, int dy, int[] consumed) {
//列表已经置顶
if (childTop == 0) {
if (!isTabsTop) {
isTabsTop = true;
if (scrollListener != null) {
scrollListener.onTabsStateChanged(isTabsTop, mChildView);
}
}
if (dy > 0 && mChildList != null) {
consumed[1] = dy;
if (!mChildList.canScrollVertically(dy)) {
//正在loading的时候不要响应上滑事件
RecyclerView.Adapter adapter = mChildList.getAdapter();
if (adapter instanceof BaseQuickAdapter) {
BaseQuickAdapter quickAdapter = (BaseQuickAdapter) adapter;
if (quickAdapter.isLoading()) {
mRootList.stopScroll();
}
}
if (!isBottom) {
isBottom = true;
if (scrollListener != null) {
scrollListener.onReachBottom(mChildList, mChildView);
}
}
} else {
//还在向下滑动,此时滑动子列表
scrollBy(dy, mChildList);
}
} else {
if (mChildList != null && mChildList.canScrollVertically(dy)) {
consumed[1] = dy;
scrollBy(dy, mChildList);
}
}
} else {
if (childTop < dy) {
//tab没有置顶,parent就消耗下面这个差值,所以父列表就只能滑动childTop了。到顶后,就是上面的逻辑了。
//childTop是tab到顶部的距离。
consumed[1] = dy - childTop;
}
if (isTabsTop) {
isTabsTop = false;
if (scrollListener != null) {
scrollListener.onTabsStateChanged(isTabsTop, mChildView);
}
}
}
}
/**
* 滑动
*
* @param dy 滑动距离
*/
private void scrollBy(int dy, RecyclerView recyclerView) {
try {
recyclerView.scrollBy(0, dy);
} catch (Exception e) {
ExceptionReporterHelper.reportException(e);
}
}
/**
* 内部列表 接受事件的处理
*/
private void onChildScrolling(int childTop, int dy, int[] consumed) {
if (childTop == 0) {
if (dy < 0) {
//tab在顶,向下滑动,如果子列表不能滑了,parent把dy都消耗掉,然后滑外部列表。
if (!mChildList.canScrollVertically(dy)) {
consumed[1] = dy;
scrollBy(dy, mRootList);
}
}
} else {
if (dy < 0 || childTop > dy) {
consumed[1] = dy;
scrollBy(dy, mRootList);
} else {
//dy大于0
consumed[1] = dy;
scrollBy(childTop, mRootList);
}
}
}
二、CoordinatorLayout 和 AppbarLayout 联动原理
我以前分享过CoordinatorLayout的使用:《Android进阶之光》Design Support Library常用控件(二):CoordinatorLayout,只懂基本的使用(其实平时开发够用了)。看了下面这两篇才是略懂原理。
《AppBarLayout滑动原理》
总结一:AppBarLayout滑动原理,手指滑动AppBarLayout时,滑动appBarlayout时,本身及内部子view不消费事件,然后事件走到CoordinatorLayout的OnTouchEvent中,接着遍历子view的behavior,因为appbarLayout通过注解添加的behavior实现了CoordinatorLayout.Behavior中定义的onStartNestedScroll/onNestedPreScroll等方法,所以appbarLayout可以通过behavior这些方法进行滑动内部子view。
《CoordinatorLayout 和 AppbarLayout 联动原理解析 》
总结二:联动原理,手指滑动recyclerView时,由于和CoordinatorLayout形成前套滑动,所以事件交给CoordinatorLayout处理,在CoordinatorLayout的OnTouchEvent中,处理方式就是总结一了,即交给AppBarLayout滑动了。那recyclerView此时也会跟着滑动,为啥呢?是因为recyclerView设置的behavior(“app:layout_behavior="@string/appbar_scrolling_view_behavior”),这个behavior的作用就纯粹为了让 recyclerView一直保持在AppBarLayout下方。(这个behavior没有实现onStartNestedScroll/onNestedPreScroll等方法。)
查看文中提到的文章,请点击“阅读原文”查看。
发文前,又看了下淘宝,貌似已经修复这个问题了,哈哈!
推荐阅读:
点个在看吧