一、Android事件分发
Android的事件从Activity开始,向顶层view进行分发,顶层view再向次级view,即它的子view进行分发,依次类推,直到找到第一个可以处理此事件的view,事件分发成功。如果,最终都没能找到合适的View,则此事件序列自动消失的。
事件派发完后进行事件的消费,如果view不处理该事件,则在onTouchEvent方法里,针对donw事件返回false,表示不消费事件。事件交还给它的父view处理,依次类推,如果都不消费,事件返回给Activity,此事件消失。事件的消费是从子view到父view再到activity。
二、NestedScrollView事件处理模型
NestedScrollView事件处理模型,是通过由消费事件的View发起和协调。父view被动接收回调,并在回调中处理消费事件,父view回调完成子view继续处理,这样反复进行,达到子view和父view交替消费事件的嵌套滑动效果。
我们看到最终的效果可能是:一次事件序列,子view滑动->父view滑动->子view滑动->父view滑动,知道事件被消费完或者达到子view分配上限3次。
通过一个简单的事件处理模型理解嵌套滑动:
- 子view消费事件前,总是先询问父view是否需要消费;
- 父view消费事件后,子view在消费剩余的部分;
- 一个事件子view最多询问父view两次,若第三次还有剩余,子view会全部消费掉。
- 中途不论父view还是子view将事件全部消费完,则事件派发提起结束。
三、NestedScrollView嵌套滑动源码分析
NestedScrollView是AndroidX中的插件,依赖AndroidX之后就可以使用该控件。
写一个简单的布局,父控件包含一个图片,一段文字,以及包裹一个子控件。子控件包含一段文字。
在子类onTouchEvent方法中实现嵌套滑动:
1、在ACTION_DOWN事件中调用startNestedScroll方法,开启嵌套滑动,这里helper是NestedScrollingChildHelper,这个是处理嵌套滑动的帮助类,使用它来简化我们需要处理的工作。
这个方法主要是帮助我们找到嵌套滑动的父类。如果是已经存在(之前已经调用并判断过)则直接返回。然后,首先判断是否开启嵌套滑动,这个是在嵌套滑动子view中通过setNestedScrollingEnabled进行设置的值。就是我们可以通过这个接口开启和关闭嵌套滑动。接下来,不断调用getParent方法,向上寻找父view,直到找到它的第一个支持嵌套滑动的父view。
在我们的父view中实现了NestedScrollingParent接口的onStartNestedScroll接口方法,并在方法中返回true。表示嵌套的父view支持嵌套滑动。
从查找嵌套滑动父view方法中,我们看到它会不断向上查找,也就是这种嵌套滑动,并不一定是父子view之间,只要子view上层存在可嵌套滑动的view,即可进行嵌套滑动,可以是子view和祖父view进行嵌套滑动。
2、在ACTION_MOVE方法中,开始进行嵌套滑动。
这里主要就是先调用dispatchNestedPreScroll方法,将事件先发给父view进行消费。
主要就是调用嵌套滑动父view的onNestedPreScroll方法,父view在这个方法中进行事件消费,并记录消费了多少事件。如果父view消费了事件,则方法返回true,否则返回false。
我们在父view重写的onNestedPreScroll方法中,对事件进行消费,滚动屏幕,并记录下滚动了多少写到consumed数组中,consumed[0]表示在x轴方向消费了多少距离,consumed[1]表示在y轴方向消费的距离。
我们在子view中拿到父view的消费情况,算出事件剩余情况,做出处理。我们的实现简单一点,如果父view没有消费,子view将事件全部消费掉。如果父view消费有剩余,子view将剩余事件全部消费掉。
3、在ACTION_UP/ACTION_CANCEL方法中调用stopNestedScroll结束嵌套滑动,这个方法会通过NestedScrollingChildHelper回调到嵌套父view的onStopNestedScroll方法。当然根据需要可以在这里进行惯性滑动的处理。
至此完成简单的嵌套滑动。
可以看到NestedScrollView框架并没有改变Android原生事件分发和消费的流程。它是完全在子view的事件处理方法中,完成整个滑动的过程。可以简单理解为在子view的onTouchEvent方法中,一会调用父类方法处理事件,一会自己处理事件。当然实际的框架比这个要复杂一些。
接下看结合流程图看一下实现一个完整嵌套滑动的方法调用流程。
四、NestedScrollView接口介绍
public interface NestedScrollingChild {
/**启用或禁用嵌套滚动的方法,设置为true,并且当前界面的View的层次结构是支持嵌套滚动的 才会触发嵌套滚动。*/
void setNestedScrollingEnabled(boolean enabled);
/**判断当前View是否支持嵌套滑动。*/
boolean isNestedScrollingEnabled();
/**表示view开始滚动了,一般是在ACTION_DOWN中调用,如果返回true则表示父布局支持嵌套滚动。通常代理给NestedScrollingChildHelper的同名方法。会触发Parent的onStartNestedScroll()方法*/
boolean startNestedScroll(@ScrollAxis int axes);
/**在事件结束比如ACTION_UP或者ACTION_CANCLE中调用,告诉父布局滚动结束。*/
void stopNestedScroll();
/**判断当前View是否有嵌套滑动的Parent。*/
boolean hasNestedScrollingParent();
/**在当前View消费滚动距离之后。通过调用该方法,把剩下的滚动距离传给父布局。如果当前没有发生嵌套滚动,或者不支持嵌套滚动,则方法无效。
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
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);
}
public interface NestedScrollingParent {
/**当NestedScrollingChild调用方法startNestedScroll()时,会调用该方法。主要就是通过返回值告诉系统是否需要对后续的滚动进行处理
child:该ViewParen的包含NestedScrollingChild的直接子View,如果只有一层嵌套,和target是同一个View
target:本次嵌套滚动的NestedScrollingChild
nestedScrollAxes:滚动方向
@return true:表示需要进行处理,后续的滚动会触发相应的回调;false: 不需要处理,后面也就不会进行相应的回调。*/
这里child和target的区别,如果是嵌套两层如:Parent包含一个LinearLayout,LinearLayout里面才是NestedScrollingChild类型的View。这个时候,child指向LinearLayout,target指向NestedScrollingChild;如果Parent直接就包含了NestedScrollingChild,这个时候target和child都指向NestedScrollingChild。
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
/**当onStartNestedScroll()方法返回true,那么紧接着就会调用该方法.它是让嵌套滚动在开始滚动之前,让布局容器(viewGroup)或者它的父类执行一些配置的初始化。*/
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
/**停止滚动,当子view调用stopNestedScroll()时回调用该方法。*/
void onStopNestedScroll(@NonNull View target);
/**子view调用dispatchNestedScroll()方法时,回调用该方法。表示开始分发处理嵌套滑动。
dxConsumed:已经被target消费掉的水平方向的滑动距离
dyConsumed:已经被target消费掉的垂直方向的滑动距
dxUnconsumed:未被tagert消费掉的水平方向的滑动距
dyUnconsumed:未被tagert消费掉的垂直方向的滑动距离 */
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
/**子view调用dispatchNestedPreScroll()方法是,回调用该方法。也就是在NestedScrollingChild在处理滑动之前,会先将机会给Parent处理。如果Parent想先消费部分滚动距离,将消费的距离放入consumed。
dx:水平滑动距离
dy:处置滑动距离
consumed:表示Parent要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
/**可以捕获对内部NestedScrollingChild的fling事件
velocityX:水平方向的滑动速度
velocityY:垂直方向的滑动速度
consumed:是否被child消费了
@return true:则表示消费了滑动事件 */
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
/**在惯性滑动距离处理之前,会调用该方法,同onNestedPreScroll()一样,也是给Parent优先处理的权利
target:本次嵌套滚动的NestedScrollingChild
velocityX:水平方向的滑动速度
velocityY:垂直方向的滑动速度
@return true:表示Parent要处理本次滑动事件,Child就不要处理了 */
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
/**返回当前滑动的方向,直接通过helper.getNestedScrollAxes()返回即可*/
int getNestedScrollAxes();
}