一.回顾下事件分发机制
在学习NestedScrollView
嵌套滑动机制之前,我们必须回顾下事件分发机制。大家可以在我之前的这一篇博客中学习完整的事件分发机制,但是在这里我就把特别需要注意的并且与这一节内容相关的知识单独拎出来。
1.图示
事件从Activity
传来,传给ViewGroup
,如果onInterceptTouchEvent
返回true
则说明ViewGroup
对事件进行拦截,则走ViewGroup
的onTouchEvent
方法,如果返回false
则说明不拦截,则调用子View
的DispatchTouchEvent
,然后调用onTouchEvent
进行事件处理。如果处理了则返回true
,如果不处理则返回false
,然后再回到ViewGroup
,如此循环往复。(详细流程见这一篇博客)
2.流程顺序
事件的分发流程是Activity-ViewGroup-View
消费流程正好反过来,是View-ViewGroup-Activity
3.事件序列
down——一系列的move——up/cancel
如果View
的down
事件没有消费,那么后续的move
事件是没办法接收到的
二.为什么需要新的嵌套滑动机制
为了解决传统事件滑动机制的bug。
我们滑动的是子View
的内容区域,而移动却是外部的ViewGroup
,所以按照传统的方式,肯定是外部的Parent
拦截了内部的Child
的事件;但是,如果要实现Parent
滑动到一定程度时,Child
又开始滑动,中间整个过程是没有间断的这样的效果,从正常的事件分发(传统机制)角度去做是不可能的,因为当Parent
拦截之后,是没有办法再把事件交给Child
的,事件分发,对于拦截,相当于一锤子买卖,只要拦截了,当前手势接下来的事件都会交给Parent
(拦截者)来处理。
所以NestedScrolling机制就应运而生,完美的解决了这个问题。
一个要记的东西
我们知道安卓的坐标系是下和右分别是y轴和x轴的正半轴。但是如果算移动的方向,一般是last-current
,比如说向上移动,那么它就是>0的,也就是dy
>0。如果向下移动,那就是<0的。也就是dy
<0。这个没什么好说的,记下来这个规定就完事了,就是正好是相反的。系统也有相应的方法,getScrollY
如果返回>0的数,就说明在上移,反之则是在下移。x轴也是一样,往左边移就是>0,往右边移就是<0
三.NestedScrollingParent和NestedScrollingChild
1.简介
那么问题来了,怎么样才能实现上面说的比较完美的嵌套滑动机制呢?没错,就是将需要实现嵌套滑动的父容器和子容器分别实现NestedScrollingParent
和NestedScrollingChild
接口。也就是当ViewGroup
充当嵌套滑动的父容器的时候,它就实现NestedScrollingParent
接口,当某ViewGroup
(比如RecyclerView
)充当嵌套滑动的子容器的时候,就实现NestedScrollingChild
。NestedScrollingParent
和NestedScrollingChild
二者是相当于并存的,只有两者同时存在才可以实现较为完美的嵌套滑动机制。(其实这也是NestedScrolling机制的一个缺点,因为参与角色只有子控件和父控件)
2.嵌套滑动机制的原理
我们最开始回顾事件分发机制的时候说了,事件的消费是从子View
开始的。所以嵌套滑动机制就立足于这里,从子View
即将消费事件开始算。它分为四个阶段,分别为初始阶段,预滚动阶段,滚动阶段,结束阶段。
如图1
如图2
总体来说就是子View
在消费之前,会问父View
到底要不要消费,所有的都是子View
去主动调用的,父View
只是被动去接收。全程由子View
去进行控制。
上面这个图可以用个很形象的例子去解释:孔融让梨(但是父亲比较贪吃)
- 就是说拿到事件之后,先找到自己的父亲(初始阶段),因为一开始不知道有没有支持嵌套滑动的父
View
,然后在子View
吃梨之前,先让父亲吃,父亲吃的话有两种情况,就是吃一部分和全吃完。如果是全吃完,那就结束了。如果是吃一部分,那剩下的就再给孩子吃。然后在子View
吃的时候,又会问一下父亲,你吃不吃,还是有两种情况。 - 这个吃就相当于
MOVE
事件的消费过程,滑动的时候如果滑动的距离不大,则全是父容器进行滑动,子容器没滑动。相当于父亲把苹果全吃了,因为父亲比较贪吃,把事件全部消费了。如果滑动的距离大,则父容器滑动到极限之后剩下的部分由子容器去滑动,相当于父亲吃了一部分苹果,剩下的苹果再还给孩子。父亲尽管比较贪吃,但是也吃不完,剩下的就给孩子吃。也就是父亲和孩子共同消费事件
关于这个,我画了一个流程图
也可以看出来全程都是孔融在让,由子View
主导。而且是让两次
3.NestedScrollingChild接口具体方法分析
NestedScrollingChild
public interface NestedScrollingChild {
/**
* 启用或禁用嵌套滚动的方法,设置为true,并且当前界面的View的层次结构是支持嵌套滚动的
* (也就是需要NestedScrollingParent嵌套NestedScrollingChild),才会触发嵌套滚动。
* 一般这个方法内部都是直接代理给NestedScrollingChildHelper的同名方法即可
*/
void setNestedScrollingEnabled(boolean enabled);
/**
* 判断当前View是否支持嵌套滑动。一般也是直接代理给NestedScrollingChildHelper的同名方法即可
*/
boolean isNestedScrollingEnabled();
/**
* 表示view开始滚动了,一般是在ACTION_DOWN中调用,如果返回true则表示父布局支持嵌套滚动。
* 一般也是直接代理给NestedScrollingChildHelper的同名方法即可。这个时候正常情况会触发Parent的onStartNestedScroll()方法
*/
boolean startNestedScroll(@ScrollAxis int axes);
/**
* 一般是在事件结束比如ACTION_UP或者ACTION_CANCLE中调用,告诉父布局滚动结束。一般也是直接代理给NestedScrollingChildHelper的同名方法即可
*/
void stopNestedScroll();
/**
* 判断当前View是否有嵌套滑动的Parent。一般也是直接代理给NestedScrollingChildHelper的同名方法即可
*/
boolean hasNestedScrollingParent();
/**
* 在当前View消费滚动距离之后。通过调用该方法,把剩下的滚动距离传给父布局。如果当前没有发生嵌套滚动,或者不支持嵌套滚动,调用该方法也没啥用。
* 内部一般也是直接代理给NestedScrollingChildHelper的同名方法即可
* dxConsumed:被当前View消费了的水平方向滑动距离
* dyConsumed:被当前View消费了的垂直方向滑动距离
* dxUnconsumed:未被消费的水平滑动距离
* dyUnconsumed:未被消费的垂直滑动距离
* offsetInWindow:输出可选参数。如果不是null,该方法完成返回时,
* 会将该视图从该操作之前到该操作完成之后的本地视图坐标中的偏移量封装进该参数中,offsetInWindow[0]水平方向,offsetInWindow[1]垂直方向
* @return true:表示滚动事件分发成功,fasle: 分发失败
*/
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);
/**
* 在当前View消费滚动距离之前把滑动距离传给父布局。相当于把优先处理权交给Parent
* 内部一般也是直接代理给NestedScrollingChildHelper的同名方法即可。
* dx:当前水平方向滑动的距离
* dy:当前垂直方向滑动的距离
* consumed:输出参数,会将Parent消费掉的距离封装进该参数consumed[0]代表水平方向,consumed[1]代表垂直方向
* @return true:代表Parent消费了滚动距离
*/
boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow);
/**
*将惯性滑动的速度分发给Parent。内部一般也是直接代理给NestedScrollingChildHelper的同名方法即可
* velocityX:表示水平滑动速度
* velocityY:垂直滑动速度
* consumed:true:表示当前View消费了滑动事件,否则传入false
* @return true:表示Parent处理了滑动事件
*/
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 在当前View自己处理惯性滑动前,先将滑动事件分发给Parent,一般来说如果想自己处理惯性的滑动事件,
* 就不应该调用该方法给Parent处理。如果给了Parent并且返回true,那表示Parent已经处理了,自己就不应该再做处理。
* 返回false,代表Parent没有处理,但是不代表Parent后面就不用处理了
* @return true:表示Parent处理了滑动事件
*/
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
NestedScrollingChild2
type
是用来区分事件类型的(touch
和fling
(惯性))
public interface NestedScrollingChild2 extends NestedScrollingChild {
boolean startNestedScroll(@ScrollAxis int axes,