- NestedScrollingParent和NestedScrollingChild这套协议的目的是为了增强(或者说反转)Android自上而下的MotionEvent传递流,这条流的传递方向是死的,一条路走到天黑不回头,单向的好处是简单,坏处就是反方向的体系内通信基本不可能了。
- 对于某些应用场景,希望在MotionEvent传递到下级以后,还有机会可以回馈给上级一些信息,就从原来的自上而下变成了自下而上,而如果要对Android原来的那套View的dispatch机制进行修改,显然代价太大,收益却不大(这个需求并不是一个common的需求),既然不能修改全局机制,那么通过接口增强功能的方式就成了首选。
- 上面说到了,既然是自下而上,那么显然需要一个下家,一个上家,而两者必然需要进行交互通信,通信就需要协议,因为两者的地位是不对等的,会有两套协议,即NestedScrollingParent(上家)和NestedScrollingChild(下家). 严格的说,其实通信是单向的,主要通信协议是NestedScrollingParent(单向通信就是被动的回调), NestedScrollingChild是对体系外部开放的通信协议。说实话,这个”Nested”前缀起的不太好,它其实只代表了一部分应用场景(比如ScrollView嵌套ScrollView),但是却没有反映出来反向通信这个本质(当然了,这里的通信内容也挺局限的,只能沟通关于Scroll的信息).
- 先看NestedScrollingParent协议中的方法(其方法全部都是on前缀,都是回调,这和上家的定位有关,上家被动的等待下家的触发回调,因为下家是事件源,下面这些回调本质就是消息的传递):
- onStartNestedScroll(View child, View target, int nestedScrollAxes): 该方法在下家触发自己的Scroll时会被调用,即下家告诉上家自己这边要开始scroll了,看上家对这一轮scroll是否感兴趣,如果感兴趣,才会传输这一轮scroll的信息,其实跟View的dispatch机制挺像的,先确认你要不要。
- onNestedScrollAccepted(View child, View target, int nestedScrollAxes),在上面的onStartNestedScroll返回true后,会被回调,给予一次初始化配置的机会。
- onStopNestedScroll(View target),下家告诉上家这一轮Scroll的结束
- onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed),每次Scroll都会回调,注意Consumed和Unconsumed,Consumed代表被下家已经消费的scroll距离,而Unconsumed则是下家”吃剩的”,才会让上家有机会处理,这里可以看到,在这个函数中上家基本不可能拦截下家对scroll的处理,上家能做的就是根据下家处理完scroll的信息做一些自己的操作
- onNestedPreScroll(View target, int dx, int dy, int[] consumed),刚刚才说了onNestedScroll中我们没有机会阻拦下家,pre则给了你阻拦的机会,其会在下家能处理自己的Scroll前被调用,关键点是consumed这个数组,上家将自己消费的scroll距离填入该数组,就变成了”下家吃上家剩的”
- onNestedFling/onNestedPreFling基本同scroll,只不过换成了fling的。
- getNestedScrollAxes
- NestedScrollingChild的接口函数基本和NestedScrollingParent是对应的,一个主动一个被动:
- setNestedScrollingEnabled(boolean enabled)/isNestedScrollingEnabled(),一个setter一个getter,表示该接口实现者是否支持NestedScroll特性,注意,如果是flying中返回false,会触发stopNestedScroll()
- startNestedScroll(int axes),该函数的实现者View需要遵从下面的约定:
- 一旦View这边开始一轮scroll,那么就需要调用此函数。该函数返回true,代表找到一个对这一轮scroll感兴趣的上家后面的联动才有可能,否则要等到下一轮scroll。
- 每一次(注意不是轮)*scroll都需要须要触发dispatchNestedPreScroll(),如果该函数返回true, 代表上家对这次scroll进行了消费,下家需要根据上家的消费值进行scroll距离的修正,当然了,你自己实现的时候,真不管也没人拦你,只要达到你的目的就行*
- dispatchNestedScroll()需要在下家消费了scroll信息后调用,将自己消费和没有消费的scroll信息都传过去,让上家能对下家消费的情况作出响应
- stopNestedScroll(), 这一轮scroll结束了
- hasNestedScrollingParent(),有上家愿意和你一块玩么?
- dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)/dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow), 上面startNestedScroll(…)中都介绍过了。
- dispatchNestedFling/dispatchNestedPreFling,基本同scroll。
- 对实现了NestedScrollingParent和NestedScrollingChild的NestedScrollView的一些简单分析(NestedScrollView实现两个接口表明了其可攻可受,也可以理解为其可以作为一个反向传递过程中的中枢,而不是一个终点):
- 按照Google的建议,上面两个接口的实现全部delegate给了NestedScrollingParentHelper和NestedScrollingChildHelper,NestedScrollView的实现中基本都是相应helper的方法进行转发。
- 上面NestedScrollingParentHelper和NestedScrollingChildHelper的构造参数都填的是NestedScrollView自己,这样才能从NestedScrollView向上不断的检查parent,找到上家。
- NestedScrollView的实现基本等同于ScrollView,主要多的就是对NestedScroll的支持,其Scroll的原理和ScrollView是一样的,都是在ComputeScroll()中结合内部的Scroller获取当前的scrollX/Y然后ScrollTo, computeScroll() -> overScrollByCompat() -> onOverScrolled() -> super.scrollTo(scrollX, scrollY)
- 来看看NestedScrollingChild的几个关键函数的调用点:
- startNestedScroll()在onInterceptTouchEvent()/onTouchEvent()中**可以开始(注意,是可以开始,即该轮交互判断为Scroll的条件已经满足了,但是呢,还没有真正的开始scroll一段距离)**scroll时都会被调用,因为是垂直滑动,会传递SCROLL_AXIS_VERTICAL.
- dispatchNestedPreScroll()的调用时机,是在onTouchEvent()中对ActionMove的处理,认为当前确实交互滑动了一定的距离,这里会本次华东的距离传递到该回调中,为了遵循约定,在其返回true的情况下(即上家消费了一定的距离),会提取出上家消费的距离值,然后对这次的MotionEvent进行offsetLocation来将上家消费的距离反映给下家
- dispatchNestedScroll()和dispatchNestedPreScroll()在同一部分,不过其所在位置是在下家已经自己处理了scroll距离之后。和前面的pre对比
- 从上面可以看到,NestedScrollView实现了通过调用自己实现的NestedScrollingChild的方法将自己在scroll/fling中的一举一动都传递了出去,并且还能根据调用函数的参数进行联动
- 跟踪下真正的实现体NestedScrollingChildHelper:
- startNestedScroll(…), 这一步很关键,沿着View体系不断向上寻找对这次NestedScroll感兴趣的上家(一定是View的parent),并保存为mNestedScrollingParent,这样后面的一系列NestedScroll消息才有传递的目的地
- while循环向上不断取view的parent,
- 对每一级parent,先调用ViewParentCompat.onStartNestedScroll(…), 如果parent实现了NestedScrollingParent接口,那么会调用其实现的onStartNestedScroll(..)方法,返回true代表该parent对这轮NestedScroll感兴趣,终于找到了上家,将其保存到mNestedScrollingParent留待后面的信息投递
- 调用ViewParentCompat.onNestedScrollAccepted(…)来回调该parent的onNestedScrollAccepted(),也符合了该函数的语义。
- dispatchNestedScroll(),每次调用都会检查isNestedScrollingEnabled()以及是否上家还在.
- 核心还是通过ViewParentCompat.onNestedScroll(…)触发上家的onNestedScroll()
- dispatchNestedPreScroll(),基本同上,offsetInWindow的部分不介绍了。
- stopNestedScroll(),除了触发上家的onStopNestedScroll(),还会将mNestedScrollingParent重置为null,代表着这一轮NestedScroll的结束
- 从上面的分析可以看出,NestedScrollingChildHelper本身没有做很复杂的工作,其意义在于将查找上家,保存上家以及在流程中触发上家的回调等比较common的操作全部独立了出来,模块化,类似于很多库中的DragHelper等辅助类的定位
- 上面将NestedScrollView**作为下家进行了分析,不过因为其也实现了NestedScrollingParent,因此也可以作为上家进行分析(即被动的接受某级子View的NestedScroll消息)**:
- 因为上家的方法都是被动回调,因此在NestedScrollView中基本不会有主动调用的地方。
- onStartNestedScroll(View child, View target, int nestedScrollAxes)的实现很简单,检查下nestedScrollAxes是不是包含了ViewCompat.SCROLL_AXIS_VERTICAL, 即对包含了垂直Scroll的NestedScroll信息感兴趣,可以和该下家进行合作
- onNestedScrollAccepted(…):
- 调用了mParentHelper的onNestedScrollAccepted(),因为上家的定位是被动的,因此NestedScrollingParentHelper中基本没有什么实现(),只是保存了NestedScrollAxe.
- startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL),这一点充分显示出NestedScrollView的中枢角色,及其现在扮演的是上家,但是通过调用此方法,继续将NestedScroll消息向上传递,对于接受了这个消息的parent来说,其又成了下家
- onNestedScroll():
- 实现NestedScroll关键的一点: scrollBy(0, dyUnconsumed), 下家没有消费掉全部的滑动距离,那么多出来的可以由上家(也就是)来进行消费,不过后面会紧接着计算NestedScrollView实际可以消费的距离,unConsumed的距离则通过,进一步调用dispatchNestedScroll(..)再次向上传递
- onNestedPreScroll(): 空函数,这应该和其定位有关:NestedScrollView先保证子View对Scroll的处理,然后才会处理剩下的,其没有对子View处理进行拦截的需求和必要
- 总结来看,NestedScrollingChild和NestedScrollingParent组成了Scroll/Fling反向传递的通道,并且可以通过一个View同时实现两者来达到形成一个反向传递链的效果,其本质很简单,只不过对与实现者的嵌入程度比较高,很难通过直接继承的方式来实现(NestedScrollView没有继承ScrollView一部分原因就是这个)