文档开发者中心
我的

讲一个 Android 嵌套滑动踩坑的真实经历

/ 今日科技快讯 /


昨日,易车网发布一份公告,声明公司董事会已收到一份初步的非约束性私有化提议。这份私有化收购要约来自由腾讯及Hammer Capital(黑马资本)组成的买方团体,提议以每股ADS(美国存托股票)16美元的现金价格收购尚未持有的所有股份。其中,腾讯目前拥有易车网约7.81%的股份,Hammer Capital未持有任何股份。声明发出后,截至9月13日美东时间收盘,易车网股价上涨8.73%,报每股14.95美元。


/ 作者简介 /


愉快的中秋假期结束啦,新的一周重新开始,很高兴又跟大家见面!


本篇文章来自琼珶和予的投稿,分享了他踩坑嵌套滑动的经历,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。


琼珶和予的博客地址:

https://juejin.im/user/5d5b50f66fb9a06b155dbfb7


/ 前言 /


本来认为自己对嵌套滑动的理解和应用还是不错的,但是最近做了一个跟手动画的需求,使用嵌套滑动发现了这里有了很多的坑,本文来根据自身的踩坑经历和经验来总结使用嵌套滑动的注意项。


本文不会介绍嵌套滑动的基本使用,不了解的同学可以参考我的文章:Android 源码分析 - 嵌套滑动机制的实现原理。


Android 源码分析 - 嵌套滑动机制的实现原理

https://www.jianshu.com/p/cb3779d36118


同时,本文嵌套滑动皆以RecyclerView为例。


/ 正文 /


1. 不要在onInterceptTouchEvent方法里面拦截事件


如果你有一个ViewGroup作为RecyclerView的父布局,这个ViewGroup主要来处理一些嵌套滑动的逻辑,比如说使用系统的SwipeRefreshLayout来做下拉刷新。如果这个ViewGroup不可能有父布局处理嵌套滑动,那么是否重写onInterceptTouchEvent可以自身需求来定,比如说SwipeRefreshLayout就重写了。


但是如果你的业务场景可能还会有ViewGroup来处理嵌套滑动,作为关系链中间的View千万不要重写onInterceptTouchEvent。


可能有对此有疑惑,现在我以一个具体的场景来解释具体的原因,假设有如下一个场景:


640?wx_fmt=png


整个事件传递的流程是:首先由RecyclerView产生嵌套滑动的事件,然后提交给SwipeRefreshLayout尝试着处理, SwipeRefreshLayout收到事件之后,发现还有父View可能会处理,然后在提交给ViewGroup,ViewGroup根据自身条件选择消费一定的距离,然后又返回给SwipeRefreshLayout,SwipeRefreshLayout在根据自身条件选择消费,最后RecyclerView在消费。整个事件传递和消费的流程如下:


640?wx_fmt=png


这里存在一种特殊情况,如果中间的SwipeRefreshLayout重写了onInterceptTouchEvent方法,导致事件不能传递到RecyclerView,从而导致了嵌套滑动的机制不能触发。有人可能有人疑问: SwipeRefreshLayout自己想拦截事件,并且处理事件,这难道有问题吗?


针对这个问题,我想说的是,正常情况下是没有问题的,但是如果ViewGroup必须跟手变化,只有ViewGroup跟手变化到最终态才能让 SwipeRefreshLayout下拉或者RecyclerView滑动,这种情况下,不走嵌套滑动的逻辑根本没法实现。


可能有人会提出相应的解决方法:我重写ViewGroup的onInterceptTouchEvent方法来拦截事件,然后消费事件不行吗?针对于这种解决方法,我想问的是,如果一次滑动产生10px的有效距离,而ViewGroup只能消费其中的5px,剩下的5px怎么办呢?根据情况传递到子View中去或者不消费?首先不消费是肯定不行的,否则就会显得滑动不灵敏,其次如果传递到子View中去,这也太麻烦了嘛。


像这种情况,我们最好的解决方法就是所有的滑动走嵌套滑动的逻辑,因为嵌套滑动本身自己支持消费部分距离的功能,而不用我们去特殊处理。


解释了在什么情况下不要重写onInterceptTouchEvent方法之后,我们现在来解释一下系统的SwipeRefreshLayout为什么要重写onInterceptTouchEvent。


  1. Google爸爸默认为SwipeRefreshLayout已经嵌套滑动关系链上最后一个View了,SwipeRefreshLayout不可能再有父View处理嵌套滑动。

  2. 重写onInterceptTouchEvent可以为SwipeRefreshLayout增加一个新特性--就是不用依赖子View就可以实现下拉刷新。也是说,我们在xml布局中直接添加一个SwipeRefreshLayout,不用给它添加子View就能下拉刷新。这也是嵌套滑动的弊端,必须得有一个View来产生嵌套滑动。


针对于上面两个原因,还是不能说服我坚持的观点--在嵌套滑动链上的View不用重写onInterceptTouchEvent方法。为什么呢?

上面的第二个问题,我们还是可以避免:既然是链上最底端的View,可以完全自己产生嵌套滑动事件,然后尝试着传递到父View,然后自己在消费,而不用去拦截事件。这样的话,整个关系链都不会破坏。所以我对系统的SwipeRefreshLayout的设计抱有迟疑态度。


2. 不要私自在dispatchTouchEvent的ACTION_CANCEL时机或者ACTION_UP时机调用stopNestedScroll方法


在解释具体原因,我们来看一下NestedScrollingChildHelper的startNestedScroll方法和stopNestedScroll方法。

stopNestedScroll方法比较简单,我们先来看看


public void stopNestedScroll(@NestedScrollType int type) {
ViewParent parent = getNestedScrollingParentForType(type);
if (parent != null) {
ViewParentCompat.onStopNestedScroll(parent, mView, type);
setNestedScrollingParentForType(type, null);
}
}


stopNestedScroll表示的意思,当前type的嵌套滑动结束了,这里主要做的是将对应的ViewParent跟重置为null。

这里为什么需要强调type呢?通常来说,在正常的滑动中,stopNestedScroll只会被调用一次,但是别忘了还有fling滑动,所以type分为两种:


1. TYPE_TOUCH,表示正常滑动,然后手指松开。
2. TYPE_NON_TOUCH,表示手指松开之后还在滑动。


所以在RecyclerView中,一次带fling操作的滑动stopNestedScroll方法会被调用两次,一次是ACTION_UP和ACTION_CANCEL调用一次,此时type为TYPE_TOUCH,一次是fling完毕,此时type为TYPE_NON_TOUCH。

那么将ViewParent重置为null有什么意义呢?这个就得从startNestedScroll方法得到答案。


public boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type) {
if (hasNestedScrollingParent(type)) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes, type)) {
setNestedScrollingParentForType(type, p);
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes, type);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}


startNestedScroll先从缓存判断是否有View可以处理,然而就是因为这个缓存会导致一个问题。

以上面的场景,SwipeRefreshLayout私自在ACTION_UP和ACTION_CANCEL调用了stopNestedScroll方法,切断了它与父View的关系链,但是没有切断它与RecyclerView的关系链,导致后面再有事件来的话,只能传递到SwipeRefreshLayout中去,而再也不能传递到SwipeRefreshLayout的父View上去。

有人说,这没事啊,RecyclerView也会在ACTION_UP和ACTION_CANCEL切断关系啊。但是有没有考虑到一种情况--就是ACTION_UP和ACTION_CANCEL事件不能传递到RecylcerView当中。

有很多场景都存在这种情况,比如说我们长按RecyclerView的ItemView然后弹出一个Dialog或者浮层,然后松开,这些都有可能导致事件不能传递到RecyclerView中去。


我们一旦在ACTION_UP和ACTION_CANCEL时切断SwipeRefreshLayout与父View的关系,但是没有切断RecyclerView与SwipeRefreshLayout的关系,整个关系链就变成这样了:

640?wx_fmt=png




点赞
2
收藏
2
回复
3
分享
举报
浏览418 发布于2019-11-02 04:03未知归属地
全部评论
最多点赞
最新发布
最早发布

顶一下,让更多的小伙伴们可以看到

4楼回复于2020-04-20 02:45 未知归属地

这帖子还是值得讨论的

3楼回复于2020-04-19 02:20 未知归属地

这帖子还是值得讨论的

2楼回复于2020-04-19 02:19 未知归属地
写回答
新增插入模板功能
一键使用模板,快速填写内容,轻松发帖~
知道了
  • 为了保障您的信息安全,请勿上传您的敏感个人信息(如您的密码等信息)和您的敏感资产信息(如关键源代码、签名私钥、调试安装包、业务日志等信息),且您需自行承担由此产生的信息泄露等安全风险。
  • 如您发布的内容为转载内容,请注明内容来源。

我要发帖子

目录 收起
1. 不要在onInterceptTouchEvent方法里面拦截事件
2. 不要私自在dispatchTouchEvent的ACTION_CANCEL时机或者ACTION_UP时机调用stopNestedScroll方法
了解社区公约,与您携手共创和谐专业的开发者社区。