事件分发机制简介

其实小编应该在前面准备一章关于Android的事件分发和处理机制的专题,因为时间原因和内容源码比较多吧(小编有强迫症,不想不贴源码),不过还是因为小编懒,哈哈。这里就对事件分发机制做一个简单的回忆,大家感兴趣的可以去看看相关资料和源码,小编后面有空也会出一期。

这里介绍一位优秀博主的博客,大家可以看这篇文章对事件分发机制与滑动冲突、以及解决方案有个全面的了解:

事件分发机制以及滑动冲突的处理

下面对事件分发与处理做一个简单总结

image-20220806150250279

关键的一点大家需要记住:事件的分发是由上层到底层,而事件的处理是由底层到上层。

  • 也就是说父View可以拦截触摸事件直接消费,而不传递给子View。
  • 子View可以处理事件,也可以选择给父类消费。
  • ViewGroup既可以消费事件,也可以分发事件
  • View只能处理事件,也就是消费。

View滑动冲突

  1. 外部滑动方向与内部滑动方向不一致

解决方案:外部拦截法,当事件传递到父View时,父View需要处理此事件,就拦截,不处理此事件就不拦截

  1. 外部滑动方向与内部滑动方向一致;

内部拦截法,当事件传递到父View时,父View都传递给子View,如果子View需要处理此事件就消耗掉,否则就交给父View处理。但是这种方法和Android事件分发不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作

  1. 上面两种情况的嵌套。

某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用
解决方案: 解决这种滑动冲突,可以用NestedScrollingParent 和 NestedScrollingChild 来解决

分析NestedScrollView机制

NestedScrollView和ScrollView类似,是一个支持滚动的控件。此外,它还同时支持作NestedScrollingParent或者NestedScrollingChild进行嵌套滚动操作。默认是启用嵌套滚动的

首先介绍两个重要接口

1.NestedScrollingParent

当可滑动的ViewGroup充当父View时实现该接口

2.NestedScrollingChild

当可滑动的ViewGroup充当子View时实现该接口

我们所熟知的RecyclerView就对上面两个接口都进行了实现,所以它才能完美的处理滑动与点击item之间的关系,同时RecyclerView还支持惯性滑动(快速滑->手指松开->滑动一段距离停下),这都是上面两个接口中封装的方法的功劳。小编后面展示的demo就比较拉了,没有实现惯性滑动效果,别问!问就是因为懒。

整个事件流程

这张图还是很清晰的展示了从点击->滑动->松开的方法调用过程

NestedScrollingChildHelper、NestedScrollingParentHelper是Google提供的帮助类,其内部是去调用ViewCompat中的方法调用(xxxCompat都是兼容处理),也就是说很多操作都可以由helper帮我们完成,对于小编这种懒人还是很友好的。

image-20220806160049703

下面对方法进行介绍:

  • onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) 当用户手指按下的时候,你是否要处理这次操作。当你确定要处理这次操作的时候,返回true;如果返回false的时候,就不会去响应后面的回调事件了。你想怎么滑就怎么话,我都不做处理。这里的(axes)滚动方向很重要,可以通过此参数判断滚动方向!

    • 参数3:直接目标,相当于能滑动的控件

    • 参数4:观察的View

    • 参数5:这个可以简单理解为滚动方向

      • ViewCompat#SCROLL_AXIS_HORIZONTAL 水平方向
      • ViewCompat#SCROLL_AXIS_VERTICAL 竖直方向
    • 参数6:这个参数是之后有的,如果你输入的类型不是TYPE_TOUCH那么就不会相应这个滚动

  • onNestedScrollAccepted(@NonNull CoordinatorLayout coordinatorLayout, @NonNull V child, @NonNull View directTargetChild, @NonNull View target, @ScrollAxis int axes, @NestedScrollType int type) 当onStartNestedScroll准备处理这次滑动的时候(返回true的时候),回调这个方法。可以在这个方法中做一些响应的准备工作!

  • onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, @NestedScrollType int type) 当滚动开始执行的时候回调这个方法。

    • 参数4/参数5:用户x/y轴滚动的距离(注意这里是每一次都回调的啊!!!)
    • 参数6:处理滚动的距离的参数,内部维护着输出距离,假设用户滑动了100px,child 做了90px的位移,你需要把consumed[1]的值改成90,这样coordinatorLayout就能知道只处理剩下的10px的滚动。其中consumed[0]代表x轴、consumed[1]代表y轴。可能你不理解这个问题,换个形象点的比喻,比如你开发某一个功能,但是你只会其中的90%那么怎么办呢?不能就不管了。好你找到了你的同事或者老大,让他去完成剩下的10%。这样问题就完美的解决了,是一个概念的!
  • onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) 上面这个方法结束的时候,coordinatorLayout处理剩下的距离,比如还剩10px。但是coordinatorLayout发现滚动2px的时候就已经到头了。那么结束其滚动,调用该方法,并将coordinatorLayout处理剩下的像素数作为参(dxUnconsumed、dyUnconsumed)传过来,这里传过来的就是 8px。参数中还会有coordinatorLayout处理过的像素数(dxConsumed、dyConsumed)。老大开始处理剩下的距离了!这个方法主要处理一些越界后的滚动。还是不懂对吧!还拿你们老大做比喻:比如上面还剩 10%的工作,这时老大处理了2%后发现已经可以上线了,于是老大结束了工作,并将处理剩下的内容(dxUnconsumed、dyUnconsumed)纪录下来,告诉你。老大处理了的内容(dxConsumed、dyConsumed)也告诉了你。

    • 参数4/参数5:当没有滚动到顶部或者底部的时候,x/y轴的滚动距离
    • 参数6/参数7:当滚动到顶部或者底部的时候,x/y轴的滚动距离
  • onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, float velocityX, float velocityY) 当手指松开发生惯性动作之前调用,这里提供了响应的速度,你可以根据速度判断是否需要进行折叠等一系列的操作,你要确定响应这个方法的话,返回true。

    • 参数4/参数5:代表相应的速度
  • onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int type) 停止滚动的时候回调的方法。当你不去响应Fling的时候会直接回调这个方法。在这里可以做一些清理工作。或者其他的内容。

再来看看两个接口的源码

1.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);
}

public interface NestedScrollingChild2 extends NestedScrollingChild {

boolean startNestedScroll(@ScrollAxis int axes, @NestedScrollType int type);

void stopNestedScroll(@NestedScrollType int type);

boolean hasNestedScrollingParent(@NestedScrollType int type);

boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow,
@NestedScrollType int type);

boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
@Nullable int[] offsetInWindow, @NestedScrollType int type);
}
public interface NestedScrollingChild3 extends NestedScrollingChild2 {

void dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
@Nullable int[] offsetInWindow, @ViewCompat.NestedScrollType int type,
@NonNull int[] consumed);
}

2.NestedScrollingParent

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);

/**
* 返回当前滑动的方向,一般直接通过NestedScrollingParentHelper.getNestedScrollAxes()返回即可
*/
@ScrollAxis
int getNestedScrollAxes();
}
public interface NestedScrollingParent2 extends NestedScrollingParent {
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);

void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes,
@NestedScrollType int type);

void onStopNestedScroll(@NonNull View target, @NestedScrollType int type);

void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, @NestedScrollType int type);

void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed,
@NestedScrollType int type);

}
public interface NestedScrollingParent3 extends NestedScrollingParent2 {

void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, @ViewCompat.NestedScrollType int type, @NonNull int[] consumed);

}

Demo

在那之前再补一张较全的事件流程图,还包括传值的过程欧,结合demo代码看也不错。

image-20220806164857678

image-20220806164945258

image-20220806165039684

效果演示

录制_2022_08_06_16_21_53_330

这里就不贴大量代码了,展示一些主要代码,详情可见文章下方的全部代码

image-20220806163510871

image-20220806163541253

总结

只要对NestedScrollView的嵌套滑动机制过程掌握,利用NestedScrollingParent、NestedScrollingChild这两个接口可以很轻松的实现自定义嵌套滑动机制,当然也可以直接通过继承自定义NestedScrollView实现一些炫酷的滑动、吸顶效果,这种效果在现在的电商平台还是很常见的。当然仅仅利用NestedScrollingParent、NestedScrollingChild实现嵌套还是有局限,只能一对一,要想实现一对多的嵌套滑动那就需要Behavior,下一章将会初步接触。

全部代码