Android 5.0时,谷歌推出NestedScrolling机制,来解决传统嵌套的缺陷
传统嵌套滑动
由于父View拦截后,后续事件无法传递到子View,导致滑动无法连贯,必须滑完父View,抬起手指,重新向上滑动
比如:ScrollView嵌套TopView+ListView,或者自定义一个LinearLayout(在onInterceptTouchEvent中判断是否拦截,在onTouchEvent中使用scrollBy进行滑动)
NestedScrolling机制
- Android5.0以上,可以直接使用ViewGoup与View自带方法
- 向下兼容,使用support v4包提供的接口
父View实现接口:NestedScrollingParent
子View实现接口:NestedScrollingChild
或者
父View实现接口:NestedScrollingParent2 / 3
子View实现接口:NestedScrollingChild2 / 3
NestedScrollingParent
@param child:<ParentView><ChildView/></ParentView>
如果这样,那么 child = target<ParentView><ViewGroup><ChildView/></ViewGroup></ParentView>
如果这样,那么 child = ViewGroup
@param target: 嵌套滚动的view (ChildView)
@param axes: 滚动方向
@return: 父View是否接收嵌套滑动
1 | boolean onStartNestedScroll( View child, |
上同,上面方法返回true才会执行此方法
1 | void onNestedScrollAccepted( View child, |
嵌套滑动结束
1 | void onStopNestedScroll(; View target) |
@param target:ChildView
@param dxConsumed: ChildView消费的x距离
@param dyConsumed: ChildView消费的y距离
@param dxUnconsumed: ChildView未消费的x距离
@param dyUnconsumed: ChildView未消费的y距离
子View滑动之后,判断父View是否继续滑动
1 | void onNestedScroll( View target, |
@param target :ChildView
@param dx: 水平滑动距离
@param dy: 垂直滑动距离
@param consumed :当前父View消耗的距离
consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离
子View滑动之前,父View优先处理
1 | void onNestedPreScroll( View target, |
@param target:ChildView
@param velocityX: x轴滚动速度
@param velocityY: y轴滚动速度
@param consumed: 子View是否消耗掉fling
@return:父View是否消耗fling
当父View不拦截fling,子View将fling传入父View
1 | boolean onNestedFling( View target, |
@param target:ChildView
@param velocityX: x轴滚动速度
@param velocityY: y轴滚动速度
@return:父View是否拦截fling
子View产生fling时,判断父View是否拦截
1 | boolean onNestedPreFling( View target, |
返回父View滑动方向
1 | int getNestedScrollAxes(); |
NestedScrollingChild
设置当前子View是否支持嵌套滑动,如果是false,父View无法响应嵌套滑动
1 | void setNestedScrollingEnabled(boolean enabled); |
当前子View是否支持嵌套滑动
1 | boolean isNestedScrollingEnabled(); |
@param axes滑动方向
@return 返回true, 表示有父View可以配合当前子View一起嵌套滑动
1 | boolean startNestedScroll(int axes); |
当前子View停止嵌套滑动
1 | void stopNestedScroll(); |
判断是否有父View
1 | boolean hasNestedScrollingParent(); |
@param dxConsumed: ChildView消费的x距离
@param dyConsumed: ChildView消费的y距离
@param dxUnconsumed: ChildView未消费的x距离
@param dyUnconsumed: ChildView未消费的y距离
父View滑动后,子View滑动后,继续将事件分发给父View来判断是否消耗
1 | boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, |
@param dx: 子View想要的水平滑动距离
@param dy: 子View想要的垂直滑动距离
@param consumed :子View传给父View的数组,记录父View消耗的距离
consumed[0] 水平消耗的距离,consumed[1] 垂直消耗的距离
子View滑动前,将事件发给父View,让父View计算消耗
1 | boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, |
@param velocityX: x轴滚动速度
@param velocityY: y轴滚动速度
@param consumed: 子View是否消耗掉fling
1 | boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); |
@param velocityX: x轴滚动速度
@param velocityY: y轴滚动速度
@return:父View是否拦截fling
如果返回true,表示父View拦截了fling,子View无法处理fling
1 | boolean dispatchNestedPreFling(float velocityX, float velocityY); |
源码分析(以NestedScrollingChild和NestedScrollingParent为例)
onTouchEvent
ACTION_DOWN
NestedScrollingChild / startNestedScroll
NestedScrollingChildHelper / startNestedScroll
while循环直到找到实现NestedScrollingParent的父View
if(ViewParentCompat / onStartNestedScroll)NestedScrollingParent / onStartNestedScroll
如果返回true,则表示已找到父View,进行如下操作
NestedScrollingChildHelper / setNestedScrollingParentForType(保存父View)
ViewParentCompat / onNestedScrollAcceptedNestedScrollingParent / onNestedScrollAccepted
Child拿到ActionDown事件,先执行startNestedScroll,通过NestedScrollingChildHelper将参数传递到Parent的回调onStartNestedScroll,如果返回ture(表示配合Child进行嵌套滑动),则还会继续回调onNestedScrollAccepted
ACTION_MOVE
NestedScrollingChild / dispatchNestedPreScroll
NestedScrollingChildHelper / dispatchNestedPreScroll
ViewParentCompat / onNestedPreScrollNestedScrollingParent / onNestedPreScroll
vNestedScrollingChild / dispatchNestedScrollNestedScrollingChildHelper / dispatchNestedScroll ViewParentCompat / onNestedScroll
NestedScrollingParent / onNestedScroll`
Child在滑动之前,调用dispatchNestedPreScroll,通过NestedScrollingChildHelper将参数传递到Parent的回调onNestedPreScroll,在这个方法中Parent可以优先于Child滑动,并记录消费的距离,如果Parent不去记录消费的距离,则距离会由Child自身消费,即Child开始滑动,会调用dispatchNestedScroll,通过NestedScrollingChildHelper将参数传递到Parent的回调onNestedScroll
ACTION_UP
NestedScrollingChild / dispatchNestedPreFling
NestedScrollingChildHelper / dispatchNestedPreFling
ViewParentCompat / onNestedPreFlingNestedScrollingParent / onNestedPreFling
NestedScrollingChild / dispatchNestedFling
NestedScrollingChildHelper / dispatchNestedFling
ViewParentCompat / onNestedFlingNestedScrollingParent / onNestedFling
NestedScrollingChild / stopNestedScroll
NestedScrollingChildHelper / stopNestedScroll
ViewParentCompat / onStopNestedScroll(置空父View)NestedScrollingParent / onStopNestedScroll
Child在fling之前会调用dispatchNestedPreFling,通过NestedScrollingChildHelper将参数传递到Parent的回调onNestedPreFling,Parent如果返回true,则Child无法处理fling,如果返回false,Child和Parent一起处理fling,这个地方体验不是很好,算是一个初代版本的bug
举例(自定义LinearLayout,包含一个topView和bottomView)
1 | <com.stew.androidtest.testfornestedscroll.NSParentLayout |
1 | public class NSParentLayout extends LinearLayout implements NestedScrollingParent { |
NestedScrollingChild2与NestedScrollingParent2
在初代版本中,fling是无法传递给parent的,parent也知不知道child的fling何时结束
现在第二个版本可以传递fling了,和第一代相比,每个方法的参数都多了一个type(除了fling)
1 | public boolean onStartNestedScroll(int axes, int type) View child, View target, |
上述5个方法内部的逻辑和第一版本一样,另外如下两个fling方法都返回false
1 | public boolean onNestedPreFling(View target, float velocityX, float velocityY) { |
总结
1.正常滑动,非fling(具体可参考RecyclerView源码onTouchEvent中的具体细节)
NestedScrollingChild / dispatchNestedPreScroll
对应
NestedScrollingParent / onNestedPreScroll
NestedScrollingChild / dispatchNestedScroll
对应
NestedScrollingParent / onNestedScroll
一个正常的滑动(down/move/up)操作下来,比如竖直方向的,dispatchNestedPreScroll和dispatchNestedScroll都会调用到,先执行dispatchNestedPreScroll,把当前一帧(16ms)的滑动距离dy分发到parent的onNestedPreScroll,parent如果消费(滑动),则consume[1]=dy,回到child,dy=dy-consume[1]=0,所以dispatchNestedScroll方法内没有进行滑动,即parent滑动,child自身不滑动,dispatchNestedPreScroll和dispatchNestedScroll这俩方法,各代表谁来滑动,关键条件就是onNestedPreScroll内写的自定义条件,条件用户自己来定
2.fling
滑动速度达到阀值,手指up之后就会进行fling操作,在fling方法内先进行了
1 | startNestedScroll(nestedScrollAxis, TYPE_NON_TOUCH); |
如果没有实现NestedScrollingParent2接口,比如第一个版本,那么就会返回false,也就不会进行
setNestedScrollingParentForType(type, p),则在后续的fling操作中,具体细节:
1 | postOnAnimation |
这个this是一个runnable,具体操作在run方法中,run方法中也有一套dispatchNestedPreScroll和dispatchNestedScroll,只不过他的驱动力是循环internalPostOnAnimation,不断地去减速执行这个runnable,可以把它想象成一个自动的减速的【手动滑动】,接着分析,在dispatchNestedPreScroll内部:
1 | final ViewParent parent = getNestedScrollingParentForType(type); |
由于没有parent,则dispatchNestedPreScroll方法返回false,所以第一版本的NestedScrollingParent实现类是无法实现fling操作的,为了解决这个bug,谷歌推出来了NestedScrollingParent2,如果parent实现了NestedScrollingParent2,则child的dispatchNestedPreScroll方法就可以顺利的打达parent的onNestedPreScroll,然后再根据自定义的条件,来决定要不要消费fling的数据,滑动部分其实原理差不多,只不过驱动力是循环执行一个runnable,而正常滑动是靠手指滑动,多走几遍过程就熟悉了,不得不说谷歌兼容设计的很巧妙