NestedScrolling机制翻译过来叫嵌套滑动机制,它提供了一种优雅解决嵌套滑动问题的方案。
嵌套同向滑动
嵌套同向滑动的问题
所谓嵌套同向滑动,就是指这样一种情况:两个可滑动的View内外嵌套,而且它们的滑动方向是相同的。

这种情况如果使用一般的处理方式,会出现交互问题,比如使用两个ScrollView进行布局,触摸内部的ScrollView进行滑动,它是滑不动的(不考虑后来Google给它加的NestedScroll开关):

分析问题原因
根据Android的触摸事件分发机制,原因很好理解:两个ScrollView嵌套时,滑动距离达到滑动手势判定阈值(mTouchSlop)的这个MOVE事件,会先经过父View的onInterceptTouchEvent()方法,父View于是直接把事件拦截,子View的onTouchEvent()方法里虽然也会在判定滑动距离足够后调用requestDisallowInterceptTouchEvent(true),但始终要晚一步。
这个效果显然是不符合用户直觉的。大部分时候,用户希望看到:当手指触摸内部ScrollView进行滑动时,能先滑动内部的ScrollView,只有当内部的ScrollView滑动到尽头时,才滑动外部的ScrollView。
这看上去非常自然,也跟触摸事件的处理方式一致,但相比触摸事件的处理,要在滑动时实现同样的效果却会困难很多。因为滑动动作不能立刻识别出来,它的处理本身就需要通过事件拦截机制进行,而事件拦截机制实质上跟从需求的角度谈触摸事件分发中第一次试造的框架一样,只是单向的,而且方向从外到内,所以无法做到先让内部拦截滑动,内部不拦截滑动时,再在让外部拦截滑动。
那能不能把事件拦截机制变成双向的呢?不是不行,但这显然违背了拦截机制的初衷,而且它很快会发展成无限递归的:双向的事件拦截机制本身是否也需要一个拦截机制呢?于是有了拦截的拦截……
尝试解决问题
换一个更直接的思路,如果我们的需求始终是内部滑动优先,那是否可以让外部View“拦截滑动的判定条件”比内部View“申请外部不拦截的判定条件”更严格,从而让滑动距离每次都先达到“申请外部不拦截的判定条件”,子View就能够在父View拦截事件前申请外部不拦截了。
能看到在ScrollView中,“拦截滑动的判定条件”和“申请外部不拦截的判定条件”都是Math.abs(deltaY) > mTouchSlop,我们只需要增大“拦截滑动的判定条件”时的mTouchSlop就可以了。但实际上这样做并不好,因为mTouchSlop到底应该增加多少是不确定的,手指滑动的快慢和屏幕的分辨率可能都会对它有影响。所以可以换一种实现,那就是让第一次“拦截滑动的判定条件”成立时,先不进行拦截,如果内部没有申请外部不拦截,第二次条件成立时,再进行拦截,这样也实现了开始的思路。于是继承ScrollView,覆写它的onInterceptTouchEvent():
1 | public class SimpleNestedScrollView extends ScrollView { |
效果如下,能看到确实实现了让内部先获取事件:

第一次优化
我们希望体验能更好一点,从上图能看到,内部即使在自己无法滑动的时候,也会对事件进行拦截,无法通过滑动内部来让外部滑动。其实内部应该在自己无法滑动的时候,直接在onTouchEvent()返回false,不触发“申请外部不拦截的判定条件”,就能让内外都有机会滑动。这个要求非常通用而且合理,在SimpleNestedScrollView基础上进行简单修改,加入代码如下所示:
1 | public class SimpleNestedScrollView extends ScrollView { |
这段代码省略了多点触控情况的判断,运行效果如下:

1 | 注意,在Android 9.0上测试,反射获取ScrollView的mTouchSlop会报错: |
这样完成了对嵌套滑动View最基本的需求:内外两层View都能滑了。事实上不用小心翼翼地让改动尽量小,既然内部优先,完全可以让内部的ScrollView在DOWN事件的时候就申请外部不拦截,然后在滑动一段距离后,如果判断自己在该滑动方向无法滑动,再取消对外部的拦截限制,思路是类似的但代码更简单:
1 | public class SimpleNestedScrollView2 extends ScrollView { |
运行效果和上面是一样的。
第二次优化
但这两种方式目前为止都没有实现最好的交互体验,最好的交互体验应该让内部不能滑动时,能接着滑动外部,甚至在你滑动过程中快速抬起时,接下来的惯性滑动也能在两个滑动View间传递。由于滑动交互的特殊性,我们可以在外部对它进行操作,所以连续滑动的实现非常简单,只要重写scrollBy就好了,在已有代码的基础上再加上下面的代码(上面的两种思路都是加一样的代码):
1 |
|
而惯性滑动的实现就会相对复杂一点,得处理computeScroll()方法,此处暂略。
小结
嵌套滑动交互主要解决下面几个问题:
- 在内部View可以滑动的时候,阻止外部View拦截滑动事件,先滑动内部View;
- 在用户一次滑动操作中,当内部View滑动到终点时,切换滑动对象为外部View,让用户能够连续滑动;
- 在用户快速抬起触发的惯性滑动中,当内部View滑动到终点时,切换滑动对象为外部View,让惯性能够连续。
NestedScrolling机制
原理
我们只考虑了给ScrollView增加支持嵌套滑动的特性,但系统开发者需要考虑给所有有滑动交互的View增加这个特性,所以一个直接的思路是在View里加入这个机制。
进一步梳理前面要解决的问题,在嵌套滑动中,是能明确区分两类作用对象的:一个是内部View,一个是外部View。而且它们的主被动关系也非常明确:因为内部View离手指更近,我们肯定希望它能优先消费事件,但同时还希望在某些情况下事件能在内部不消耗的时候给外部消耗,这当然也是让内部来控制,所以内部是主动,外部是被动。由此整个嵌套滑动的过程可以认为是这样的:触摸事件交给内部View进行消费,内部View执行相关逻辑,在合适的时候对外部View进行一定的控制,两者配合实现嵌套滑动。这就包括了两部分逻辑:
- 内部View中的主动逻辑,需要主动阻止外部View拦截事件,自己进行滑动,并在合适的时候让外部View配合进行剩下的滑动;
- 外部View中的被动逻辑,基本就是配合行动。
由于View里是不能放其它View的,它只能是内部的、主动的角色,而ViewGroup既可以放在另一个ViewGroup里,它里边也可以放其它的View,所以它可以是内部的也可以是外部的角色。这正好符合View和ViewGroup的继承关系,所以一个很自然的设计是在View中加入主动逻辑,在ViewGroup中加入被动逻辑。
并不是每个View和ViewGroup都能够滑动,滑动只是众多交互中的一种,所以Google加入的这些逻辑其实都是帮助方法,相关的View需要选择在合适的时候进行调用,最后才能实现嵌套滑动的效果。Google希望能帮助我们实现一个什么样的嵌套滑动效果:
- 从逻辑上区分嵌套滑动中的两个角色:nested scroll child和nested scroll parent,对应了内部的View和外部的View。为什么叫逻辑上?因为实际上它允许一个View同时扮演两个角色;
- nested scroll child会在收到DOWN事件时,找到自己父辈中最近的能与自己匹配的nested scroll parent,与它进行绑定并关闭它的事件拦截机制;
- 然后nnested scroll child会在接下来的MOVE事件中判定出用户触发了滑动手势,并把事件流拦截下来给自己消费;
- 消费事件流时,对于每一次MOVE事件增加的滑动距离:
- nested scroll child并不是直接自己消费,而是先把它交给nested scroll parent,让nested scroll parent可以在nested scroll child之前消费滑动;
- 如果nested scroll parent没有消费或是没有消费完,nested scroll child再自己消费剩下的滑动;
- 如果nested scroll child自己还是没有消费完这个滑动,会再把剩下的滑动交给nested scroll parent消费;
- 最后如果滑动还有剩余,nested scroll child可以做最终的处理
- 同时在nested scroll child的computeScroll()方法中,nested scroll child也会把自己因为用户fling操作引发的滑动,与上面用户滑动屏幕触发的滑动一样,使用“parent -> child -> parent -> child”的顺序进行消费。
这跟我们自己实现嵌套滑动的方式非常像,但它这些地方做得更好:
- nested scroll child使用更灵活的方式找到和绑定自己的nested scroll parent,而不是直接找自己的上一级节点;
- nested scroll child在DOWN事件时关闭nested scroll parent的事件拦截机制单独用了一个Flag进行关闭,这就不会关闭nested scroll parent对其它手势的拦截,也不会递归往上关闭父辈们的事件拦截机制。nested scroll child直到在MOVE事件中确定自己要开始滑动后,才会调用requestDisallowInterceptTouchEvent(true)递归关闭父辈们全部的事件拦截;
- 对每一次MOVE事件传递来的滑动,都使用“parent -> child -> parent -> child”机制进行消费,让nested scroll child在消费滑动时与nested scroll parent配合更加细致、紧密和灵活;
- 对于因为用户fling操作引发的滑动,与用户滑动屏幕触发的滑动使用同样的机制进行消费,实现了完美的惯性连续效果。
使用
Google给View和ViewGroup加入的需要我们关心的方法一共有这些(只注明了关键返回值和参数):
1 | //View |
怎么调用这些方法取决于要实现什么角色。
- 在实现一个nested scroll child角色时:
- 在实例化的时候调用setNestedScrollingEnabled(true),启用嵌套滑动机制;
- 在DOWN事件时调用startNestedScroll()方法,它会找到自己父辈中最近的与自己匹配的nested scroll parent,进行绑定并关闭nested scroll parent的事件拦截机制;
- 在判断出用户正在进行滑动后:
- 先常规操作:关闭父辈们全部的事件拦截,同时拦截自己子View的事件;
- 然后调用dispatchNestedPreScroll()方法,传入用户的滑动距离,这个方法会触发nested scroll parent对滑动的消费,并且把消费结果返回;
- 然后nested scroll child可以开始自己消费剩下滑动;
- nested scroll child自己消费完后调用dispatchNestedScroll()方法,传入最后没消费完的滑动距离,这个方法会继续触发nested scroll parent对剩下滑动的消费,并且把消费结果返回;
- nested scroll child拿到最后没有消费完的滑动,做最后的处理,比如显示overscroll效果,比如在fling的时候停止scroller。
- 如果希望惯性滑动也能传递给nested scroll parent,那么在View的computeScroll()方法中,对于每个scroller计算到的滑动距离,与MOVE事件中处理滑动一样,按照这个顺序进行消费:“dispatchNestedPreScroll() -> 自己 -> dispatchNestedScroll() -> 自己”;
- 在UP、CANCEL事件中以及computeScroll()方法中惯性滑动结束时,调用stopNestedScroll()方法,这个方法会打开nested scroll parent的事件拦截机制,并取消与它的绑定。
- 在实现一个nested scroll parent角色时:
- 重写方法boolean onStartNestedScroll(View child, View target, int nestedScrollAxes),通过传入的参数,决定自己对这类嵌套滑动感兴趣,在感兴趣的情况中返回true,nested scroll child就是通过遍历所有nested scroll parent的这个方法来找到与自己匹配的nested scroll parent;
- 如果选择了某种情况下支持嵌套滑动,那么在拦截滑动事件前,调用getNestedScrollAxes(),它会返回某个方向的拦截机制是否已经被nested scroll child关闭了,如果被关闭,就不应该拦截事件了;
- 开启嵌套滑动后,可以在onNestedPreScroll和onNestedScroll方法中等待nested scroll child的消息,它对应了在nested scroll child中调用的dispatchNestedPreScroll和dispatchNestedScroll方法,可以在有必要的时候进行自己的滑动,并且把消耗掉的滑动距离通过参数中的数组返回。
在我们刚开始的同向嵌套滑动的例子中,只要打开ScrollView的setNestedScrollingEnabled(true)开关,就能看到嵌套滑动的效果(实际上ScrollView实现的不是完美的嵌套滑动,原因见下一节):

以上都说的是单一角色时的使用情况,有时候会需要一个View扮演两个角色,就需要再多做一些事情,比如对于nested scroll parent,要时刻注意也是 nested scroll child,在有滑动的时候也照顾一下自己的nested scroll parent。
版本历史简述
版本一
第一个版本,2014年9月
在Android5.0/API 21(2014.9) 时,Google第一次加入了NestedScrolling机制。
虽然在版本更新里完全没有提到,但是在View和ViewGroup的源码里已经能看到其中的嵌套滑动相关方法。而且此时使用了这些方法实现了嵌套滑动效果的View其实已经有不少了,除了ScrollView,还有AbsListView、ActionBarOverlayLayout等,而这些也基本是当时所有跟滑动有关的View了。所以,在Android5.0时其实就能通过setNestedScrollingEnabled(true)开关启用View的嵌套滑动效果。这是NestedScrolling机制的第一版实现。
重构第一个版本,2015年4月
因为第一个版本的NestedScrolling机制是加在framework层的View和ViewGroup中,所以能享受到嵌套滑动效果的只能是Android5.0的系统,也就是当时最新的系统。大家都知道,这样的功能不会太受开发者喜欢,所以在当时NestedScrolling机制基本没有怎么被使用(所以大家一说嵌套滑动就提后来才发布的NestedScrollView而不知道ScrollView早就能嵌套滑动也是非常正常了)。
Google后面重构出来两个接口(NestedScrollingChild、NestedScrollingParent)、两个Helper(NestedScrollingChildHelper、NestedScrollingParentHelper)外加一个NestedScrollView,在Revision 22.1.0 (2015.4)到来之际,把它们一块加入了v4 support library。
随后,在下一个月Revision 22.2.0 (2015.5)时,Google又推出了Design Support library,其中的控件CoordinatorLayout更是把NestedScrolling机制使用得出神入化。
这时的NestedScrolling机制相比之前放在View和ViewGroup中的第一个版本,其实完全没有改动,只是把View和ViewGroup里的方法分成两部分放到接口和Helper里了,NestedScrollView里跟嵌套滑动有关的部分也跟ScrollView里的没什么区别,所以此时的NestedScrolling机制本质还是第一个版本,只是形式发生了变化。而NestedScrolling机制形式的变化带来了如下一些影响:
- 因为这个机制其实不涉及核心的framework层的东西,所以让它脱离API版本存在,让低版本系统也能有嵌套滑动的体验,才是导致这个变化的主要原因也是它的主要优点。至于依赖倒置、组合大于继承应该都只是结果。而便于修复Bug什么的Google当时大概也没有想到。
- 这么做的主要缺点有:
- 使用麻烦。这是肯定的,本来放在View里拿来就用的方法,现在不仅要实现接口,还要自己去写接口的实现,虽然有Helper类进行辅助,但还是麻烦;
- 暴露了更多内部的不需要普通使用者关心的API。这点比上一点要重要一些,因为它会影响开发者对整个机制的上手速度。本来,如前文介绍,只需要知道有这9个方法就可以,现在这一改,只child里就有9个,parent里还有8个,接近二倍了。多的这些方法中有的是机制内部用来沟通的(比如isNestedScrollingEnabled()、onNestedScrollAccepted()),有的是设计别扭用得很少的(比如dispatchNestedFling()),有的是需要特别优化细节才需要的(比如hasNestedScrollingParent()),一开始开发者其实完全不用关心。
第一个版本的Bug
第一版NestedScrolling机制有一个的著名Bug:惯性不连续。
简单说就是在滑动内部View时快速抬起手指,内部View会开始惯性滑动,当内部View惯性滑动到自己顶部时便停止了滑动,此时外部的可滑动View不会有任何反应,即使外部View可以滑动。本来这个体验也没多大问题,但因为手动滑动的时候,内部滑动到顶部时可以接着滑动外边的View,这就形成了对比,有对比就有差距,在惯性滑动的时候也需要把里面的滑动传递到外面去。所以这个问题也不能算是Bug,只是体验没有做到那么好罢了。其实Google也考虑过惯性,其中关于fling的4个 API便表明了用来处理惯性滑动:
- nested scroll child:dispatchNestedPreFling、dispatchNestedFling
- nested scroll parent:onNestedPreFling、onNestedFling
前面的内容中说nested scroll child直接消费用户快速抬起时产生的惯性滑动,在computeScroll方法中把惯性引起的滑动也传递给nested scroll parent,让父子配合进行惯性滑动。
但实际上此时的NestedScrollView是这么写的:
1 | public boolean onTouchEvent(MotionEvent ev) { |
同滑动一样,设计者给惯性(速度)也设计了一套协同消费的机制。但这套机制与滑动不太一样。在用户滑动nested scroll child并快速抬起手指产生惯性的时候,flingWithNestedDispatch()方法中,nested scroll child会先问nested scroll parent是否消费此速度,如果消费,就把速度全部交出,自己不再消费
如果nested scroll parent不消费,那么将再次把速度交给nested scroll parent,并且告诉它自己是否有消费速度的条件(根据系统类库一贯的写法,如果nested scroll child消费这个速度,nested scroll parent都不会对这个速度做处理),同时自己在有消费速度的条件时,对速度进行消费。自己消费速度的方式是使用mScroller进行惯性滑动,但是在computeScroll()中并没有把滑动分发给nested scroll parent,最后只要抬起手指,就会调用stopNestedScroll()解除与nested scroll parent的绑定,宣告这次协同合作到此结束。
惯性的这套协同消费机制只能在惯性滑动前让nested scroll parent有机会拦截处理惯性,它并不能在惯性滑动过程中让nested scroll child和nested scroll parent协同消费惯性引发的滑动,也就实现不了前面期望的惯性连续效果。实现惯性连续的方式其实非常简单,不需要增加新的机制,直接通过滑动的协同消费机制,在nested scroll child进行惯性滑动时,把滑动传递出来,就可以了。所以修复这个Bug也很简单,只是比较繁琐:修改所有作为nested scroll child角色使用了嵌套滑动机制的系统控件,惯性相关的API和处理逻辑都可以保留,只要在computeScroll()中把滑动用dispatchNestedPreScroll()和dispatchNestedScroll()方法分发给nested scroll parent,再更改一下解除与nested scroll parent绑定的时机,放在fling结束之后就可以了。自己写的nested scroll child View可以直接改,但系统提供的NestedScrollView、RecyclerView等控件,就只能提个issue等官方修复或者拷贝一份出来自己改。
版本二
第二个版本,2017年9月
2017年9月,Revision 26.1.0更新了一版NestedScrollingChild2和NestedScrollingParent2,并且处理了第一版中系统控件的Bug,这便是第二个版本的NestedScrolling机制了。
首先在接口上,nested scroll child在computeScroll中分发滑动给nested scroll parent,这是关键,并且区分开是用户手指移动触发的滑动还是由惯性触发的滑动。第二版中给所有NestedScrollingChild中滑动相关的(确切地说是除了fling相关、滑动开关外的)5个方法、所有NestedScrollingParent中滑动相关的(确切地说是除了fling相关、获取滑动轴外的)5个方法,都增加了一个参数type,type有两个取值代表上述的两种滑动类型:TYPE_TOUCH、TYPE_NON_TOUCH。第二版的两个接口没有增删任何方法,只是给10个方法加了个type参数,并且对旧的接口做了个兼容,让它们的type是TYPE_TOUCH。
Helper类也要修改,第一版的NestedScrollingChildHelper里本来持有了一个nested scroll parent域mNestedScrollingParentTouch,作为绑定关系,第二版再加了一个nested scroll parent域mNestedScrollingParentNonTouch,为什么是两个而不公用一个,大概是避免对两类滑动的生命周期有过于严格的要求,比如在NestedScrollView的实现里,就是先开启TYPE_NON_TOUCH类型的滑动,然后关闭了TYPE_TOUCH类型的滑动,如果公用一个nested scroll parent 域就做不到这样了。NestedScrollingChildHelper里主要就做了这一点额外的改动,其它的改动都是增加参数后的常规变换,NestedScrollingParentHelper里就更没有特别的变化了。
以NestedScrollView为例,主要的变化如下:
1 | public boolean onTouchEvent(MotionEvent ev) { |
简单说明一下,UP时候做的事情没有变,还是在这解除了与nested scroll parent的绑定,但是注明了类型是TYPE_TOUCH;在fling()方法中,调用startNestedScroll()开启了新一轮绑定,不过这时的类型变成了TYPE_NON_TOUCH;最多的改动是在computeScroll()方法中,但逻辑很清晰:对于每个dy,都会经过“parent -> child -> parent -> child”这个消费流程,从而实现了惯性连续,解决了Bug。
第二个版本的Bug
第一个二倍速的问题正好出现在了NestedScrollView中,RecyclerView等类没有这个问题,它的现象是这样:当外部View不在顶部、内部View在顶部时,往下滑动内部View然后快速抬起(制造fling),预期效果应该是外部View往下进行惯性滑动,实际上有一点点区别,外部View往下滑动的速度会比预想中要快,大概是两倍的速度(反方向也是一样)。
如果把第二版嵌套滑动机制更新的NestedScrollView跟之前的对比,会很容易发现flingWithNestedDispatch()中,fling(velocityY)前的if(canFling)消失了,这个if判断在新的机制中并不需要去掉。没有了if会让外部View同时进行两个fling,实际体验也确实是这样。所以解决这个问题很简单,直接把if判断补上就好了。
第二个问题在所有的嵌套滑动控件都存在,而且体验非常明显。当外部View在顶部、内部View也在顶部时,往下滑动内部View然后快速抬起(制造fling),(目前什么都不会发生,因为都滑到顶了,关键是下一步)马上滑外部View,预期应该是外部View往上滚动,但实际上会发现滑不动它,或是滑上去一点,马上又下来了,像是有一台无形的马达在跟你的手指较劲(反方向也是一样)。
原因解释起来也非常简单:看方法flingWithNestedDispatch()中的代码:其中的dispatchNestedPreFling()大部分时候会返回false,于是几乎所有的情况下,内部View都会通过fling()方法启动自己mScroller这个小马达,然后在小马达启动后,到computeScroll()方法中,如果不直接触摸内部View,除非等到马达自己停止,否则没有外力能让它停下,于是它会一直向外输出dispatchNestedPreScroll()和dispatchNestedScroll()。所以在上面的现象中,即使内外的View都在顶部,都无法滑动,内部View的小马达还在工作,只要你把外部View滑到不在顶部的位置,它就又会把它给滑下来。其实不需要前面说的“当外部View在顶部、内部View也在顶部时”这种场景(这只是最好复现的场景),当以任何方式开启了内部View的小马达后,又不通过直接触摸内部View把它关闭时,都能看到这个问题。
除了用户直接触摸内部View让它停止,它还需要有一个停止开关,至少让用户触摸外部View的时候也能关闭它,更合理的实现还应该让驱动过程能够反馈,当出现情况无法驱动(比如内外都滑到顶部)时,停下马达。
前文讲过,这个机制中nested scroll child是主动的一方,nested scroll parent完全是被动的,nested scroll parent没法主动通知nested scroll child自己滑不动了。但nested scroll parent并不是没办法告知nested scroll child信息,通过方法的返回值和引用类型的参数,nested scroll child仍然可以从nested scroll parent中获取信息。所以只要给NestedScrolling机制加一组方法,让nested scroll child询问nested scroll parent是否能够滑动,问题就解决了,如果nested scroll parent滑不动了,nested scroll child自己也滑不动,那就赶紧关闭马达。
版本三
第三个版本,2018年11月
在2018年11月5日androidx.core 1.1.0-alpha01的更新中,Google给出了最新的修复——NestedScrollingChild3和NestedScrollingParent3,以及一系列系统组件也陆续进行了更新。
先看接口,哪里不通改哪里,在接口NestedScrollingChild3中,没有增加方法,只是给dispatchNestedScroll方法增加了一个参数int[] consumed,并且把它的boolean返回值改成了void,有了能获取更详细信息的途径,自然就不需要这个boolean了;接口NestedScrollingParent3同样只是改了一个方法,给onNestedScroll增加了int[] consumed参数(它返回值就是 void,没变)。
下面是NestedScrollingChild3中的对比:
1 | // 2 |
再看下Helper,NestedScrollingChildHelper除了适配新的接口基本没有改动,NestedScrollingParentHelper也只是增强了一点逻辑的严谨性。
最后再看NestedScrollView,改动部分跟预期基本一致:
1 |
|
修改最多的还是computeScroll(),不过其它地方也有些变化,简单说明一下:因为onNestedScroll()增加了记录距离消耗的参数,所以nested scroll parent就需要把这个数据记录上并且继续传递给自己的nested scroll parent;flingWithNestedDispatch()方法中,本来的预期是恢复第一版的写法,把fling(velocityY)前的if (canFling)加回来,结果连canFling也不判断了,dispatchNestedFling(0, velocityY, true)直接传true,fling(velocityY)始终调用。这意味着什么呢?需要结合大部分View的写法来看。搜索API 28的代码就会看到,对于onNestedPreFling()方法,除了ResolverDrawerLayout会在某些情况下消费fling并返回true,以及CoordinatorLayout会象征性地问一遍自己孩子们的Behavior,其它的写法都是直接返回false;对于onNestedFling(boolean consumed)方法,所有的写法都是,只要consumed为true,就什么都不会做,这种做法也非常自然。所以当前的现状是,绝大部分情况下,内部View的fling小马达都会启动,外部View都不会消费内部View产生的fling。这就代表着:惯性的协作机制完全被滑动的协作机制取代了。当然,即使名存实亡,但如果真的有特殊需求需要使用到fling的传递机制,也是可以使用的。最后来看computeScroll(),它基本把之前文章中说怎么修复第二版中Bug时的思路实现了:因为能从dispatchNestedPreScroll()和dispatchNestedScroll()得知nested scroll parent消耗了多少这一次分发出去的滑动距离,同时也有自己消耗了多少,两者一合计,如果还有没消耗的滑动距离,那肯定无论内外都滑到头了,于是就把小马达关停。
第三个版本的Bug
对比代码容易看到,第三版中DOWN事件的处理相对第二版没有变化,它没有加入触摸外部View后关闭内部View马达的机制,更确切地说是没有加入“触摸外部View后阻止对内部View传递过来的滑动进行消费的机制”。所以只有外部View滑动到尽头的时候才能关闭马达,外部View没法给内部View反馈自己被摁住了。
不过这个问题只有可以响应触摸的nested scroll parent需要考虑,可以响应触摸的nested scroll parent主要就是NestedScrollView了,所以这个问题主要还是NestedScrollView的问题。这个问题也非常好改,只需要在DOWN事件后能给nested scroll child反馈自己被摁住了就可以,可以用反射,或是直接把NestedScrollView挪出来改,关键代码如下:
1 | private boolean mIsBeingTouched = false; |
参考资料:
RubiTree 【透镜系列】看穿 > NestedScrolling 机制 >