什么是嵌套滑动
这是某个外卖App的点餐界面,它实现了这样一种滑动效果:
- 当顶部标签未完全隐藏时,滑动的是整个ViewGroup。
- 当顶部标签完全隐藏时,滑动的是列表内容。
按照正常的事件分发机制,你可能会想利用onInterceptTouchEvent()方法,在顶部标签未完全隐藏时由ViewGroup将触摸事件拦截,在顶部标签完全隐藏时将触摸事件分发给子view来解决这个问题。但事实上这是不可能的。考虑这样一种情况:在ACTION_DOWN时顶部标签还未完全隐藏,此时ViewGroup通过onInterceptTouchEvent()拦截了触摸事件。如果这样做了,子View就完全没有处理ACTION_DOWN的机会,也就无法收到后续的任何触摸事件,自然也就不可能实现滚动子view的内容的效果。
为解决这一问题,Google在support.v4包中提供了两个接口,即标题中的NestedScrollingParent与NestedScrollingChild。从名字上就可以看出,NestedScrollingParent是提供给ViewGroup实现的,NestedScrollingChild是提供给View实现的。同时,为了简化实现这两个接口的工作,support.v4包中同时还提供了NestedScrollingParentHelper与NestedScrollingChildHelper两个帮助类,里面的方法和接口中对应的方法同名。本文的主要内容就是分析这两个接口与两个帮助类的源码,弄清楚它们的内部逻辑,为实现嵌套滑动打好基础。
NestedScrollingParent
首先看看提供给ViewGroup实现的NestedScrollingParent中包含的方法:
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
在子view发出嵌套滑动请求时被调用。
child:这个ViewGroup的含有target的直接子view;
target:发出嵌套滑动请求的view;
nestedScrollAxes:指明滑动方向,可能为ViewCompat.SCROLL_AXIS_HORIZONTAL(水平滑动)或是
ViewCompat.SCROLL_AXIS_VERTICAL(垂直滑动)。
返回值:true代表同意进行嵌套滑动。
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
在同意了子view的嵌套滑动请求后被调用,也就是onStartNestedScroll()返回true之后。
参数同onStartNestedScroll()。
public void onStopNestedScroll(View target);
嵌套滑动结束后被调用。
target:发起嵌套滑动的view。
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,int dxUnconsumed, int dyUnconsumed);
嵌套滑动过程中被调用,此时target已经消费掉部分滑动。
target:发起嵌套滑动的view;
dxConsumed:被target消费掉的水平滑动;
dyConsumed:被target消费掉的垂直滑动;
dxUnconsumed:未被target消费掉的水平滑动;
dyUnconsumed:未被target消费掉的垂直滑动。
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
在target消费滑动之前被调用。
target:发起嵌套滑动的view;
dx:水平滑动距离;
dy:垂直滑动距离;
consumed:被消费掉的滑动距离。注意这个参数是用作输出的,在执行完具体的滑动逻辑之后,将实际消费掉的水平滑动距离与垂直滑动距离填在consumed[0]与consumed[1]中。
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
在子view监测到一次抛动后被调用。
target:发起嵌套滑动的view;
velocityX:水平速度;
velocityY:垂直速度;
consumed:target是否消费了这次抛动;
返回值:true代表父view对这次抛动作出了响应。
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
和前面的相似,不过这个是在target消费抛动之前。
public int getNestedScrollAxes();
返回本次嵌套滑动的方向。
可以发现,NestedScrollingParent里面基本都是一些回调方法。接下来看看NestedScrollingParentHelper能帮我们做些什么:
private int mNestedScrollAxes;
public void onNestedScrollAccepted(View child, View target, int axes) {
mNestedScrollAxes = axes;
}
public int getNestedScrollAxes() {
return mNestedScrollAxes;
}
public void onStopNestedScroll(View target) {
mNestedScrollAxes = 0;
}
好吧,它基本上做不了什么,只是帮忙管理了关于滑动方向的信息而已。实际上这也是可以理解的,因为只有这样,才能够根据实际需要作出不同的响应,这也是回调方法存在的最大意义。
从前面介绍的方法中也能看出,嵌套滑动是由子view发起的,主动权握在子view手上,父view仅仅是在特定的时间点作出响应而已。接下来就来看看NestedScrollingChild接口提供了些什么方法。
NestedScrollingChild
不同于NestedScrollingParentHelper,NestedScrollingChildHelper实现了NestedScrollingChild接口中的所有方法。也就是说,当我们需要实现NestedScrollingChild接口时,只需要在对象内部创建一个NestedScrollingChildHelper,然后把所有方法的实现都交给它就行了。因此,我们直接从NestedScrollingChildHelper入手,看看它提供了些什么方法,以及它们是如何实现的。
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
//停止当前滑动
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
public boolean isNestedScrollingEnabled() {
return mIsNestedScrollingEnabled;
}
设置是否允许启动嵌套滑动,以及当前是否允许嵌套滑动。