Android事件分发机制与核心用法

前言

作为一名移动开发,我们对滑动冲突可以说是屡见不鲜。虽然Android已经提供了诸如NestedScrollView、CoordinatorLayout等支持嵌套滑动的组件,但其实并不能覆盖所有的滑动场景,我们终归会遇到需要自己去解决的滑动冲突。这篇文章将阐述如何处理常见的滑动冲突,而滑动冲突的处理本质上就是处理事件分发,所以我们从事件分发讲起,一步一步斩首滑动冲突。

事件分发

何为事件分发?

事件指的是屏幕触发事件——即Android中的TouchEvent/MotionEvent。每一次我们触摸屏幕,都会产生一连串的触摸事件,这些一连串的触摸事件合起来就是一个触摸事件序列。
触摸事件在Android官方API中由类MotionEvent来描述,不同的触摸事件对应不同的事件类型。事件类型分别有ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL。
那什么叫分发呢?我们都知道Android是由View树进行渲染的。假设屏幕坐标为(11,11)的区域既属于一个LinearLayout,又属于LinearLayout下的一个Button,那我这次触碰所产生的触摸事件,是该给LinearLayout还是Button呢?当然,我们很确定这次触摸事件最终会被Button所处理。那触摸事件是怎么给到Button的呢?需要经过LinearLayout吗?怎样能让Button不处理呢?这就需要我们了解触摸事件(后文统称为事件)在View树上传递与消费的过程,这就是事件的分发。
#事件分发机制
下面我们就详述事件是如何在View树上进行分发的。当然,除了View树外,还有Activity、Dialog等组件也会对事件进行处理,但他们都是用View树进行最终的渲染的,所以这里只拿Activity进行举例,结合 ViewGroup与View一起看。

三个核心方法

dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

Android就是靠这三个方法,解决了事件的分发。注意onInterceptTouchEvent()方法只有ViewGroup才会有,dispatchTouchEvent()、onTouchEvent()是Activity、ViewGroup、View都有的。它们的返回值都是布尔类型,但返回值代表的意义却不尽相同,下面我们会讲到。

ACTION_DOWN的分发

下面我们以ACTION_DOWN事件为例,讲解ACTION_DOWN事件的分发过程。

在各自的方法上加上日志:
Acitivity:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "Activity dispatchTouchEvent return super")
    return super.dispatchTouchEvent(ev)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.i("touch_event_test", "Activity onTouchEvent return super")
    return super.onTouchEvent(event)
}

ViewGroup1:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup1 dispatchTouchEvent return super")
    return super.dispatchTouchEvent(ev)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup1 onTouchEvent return super")
    return super.onTouchEvent(event)
}

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup1 onInterceptTouchEvent return super")
    return super.onInterceptTouchEvent(ev)
}

ViewGroup2:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup2 dispatchTouchEvent return super")
    return super.dispatchTouchEvent(ev)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup2 onTouchEvent return super")
    return super.onTouchEvent(event)
}

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "ViewGroup2 onInterceptTouchEvent return super")
    return super.onInterceptTouchEvent(ev)
}

View:

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    Log.i("touch_event_test", "View dispatchTouchEvent return super")
    return super.dispatchTouchEvent(ev)
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
    Log.i("touch_event_test", "View onTouchEvent return super")
    return super.onTouchEvent(event)
}
默认返回值

这三个方法,默认都是返回的super.xxx()。这里我们先触碰一下View的区域,从日志上看一下事件是怎么分发的:

可以看到默认情况下,ACTION_DOWN事件的分发遵循以下流程图:

这个流程图可以看成是一个U型图,从Activity的dispathchTouchEvent方法开始,如果都是返回super的话,会一直到Activity的onTouchEvent结束。

改变dispatchTouchEvent返回值

现在我们将ViewGroup2的dispatchTouchEvent返回值设置为false,看看会发生什么:

可以看到,当ViewGroup2的dispatchTouchEvent返回false时,TouchEvent会传递给父View(即ViewGroup1)的onTouchEvent方法,然后再继续往上分发。用流程图表示:

再将ViewGroup2的dispatchTouchEvent返回值设置为true,日志:

可以看到,当ViewGroup2的dispatchTouchEvent返回true时,TouchEvent会直接在这里消费掉,不再继续分发。流程图:

改变onTouchEvent返回值

下面我们改变ViewGroup2的onTouchEvent的返回值,另外两个方法都是返回super。先将返回值设为false,日志:

可以看到,当onTouchEvent返回false时,事件分发跟默认返回super是一样的,仍然是按U型链进行分发,将事件传递给父View的onTouchEvent。流程图:

当我们将onTouchEvent返回值设置为true时,日志:

可以看到,当onTouchEvent返回true时,事件就在这里消费掉了,不再继续分发。流程图:

改变onInterceptTouchEvent返回值

现在我们设置ViewGroup2的onInterceptTouchEvent返回值为false,观察日志:

可以看到,onInterceptTouchEvent返回false跟onTouchEvent返回false是一样的,都是以默认的路径继续分发事件。流程图:

下面设置onInterceptTouchEvent返回值为true,观察日志:

可以看到,当ViewGoup2的onInterceptTouchEvent返回true时,事件被分发到了ViewGoup2的onTouchEvent方法,但没有消费掉,而是继续分发。流程图:

之前有提到,只有ViewGroup才会有onInterceptTouchEvent方法。为什么呢?因为我们可以从上面的几个流程图中看到,Activity的dispatchTouchEvent方法在返回false时,事件会分发到Activity的onTouchEvent方法。View的dispatchTouchEvent方法在返回super或者false时,事件也同样能分发到View的onTouchEvent方法。而ViewGroup的dispatchTouchEvent方法在返回false和super时是将事件往下分发,在返回true时是直接消费,通过改变dispatchTouchEvent方法的返回值根本不能直接分发到自己的onTouchEvent方法。所以针对ViewGroup会有一个onInterceptTouchEvent方法,来让它可以选择将事件分发给自己的onTouchEvent方法。

小结

事件的三个方法在View中默认返回都是super,按U型链进行分发。
对于dispatchTouchEvent方法
返回true,消费事件
返回false,如果不是Activity,会将事件分发到上一级View的onTouchEvent。如果是Activity,因为没有上一级View了,就会直接消费事件。
对于onTouchEvent方法:
返回true,消费事件
返回false,将事件分发到上一级View的onTouchEvent
对于onInterceptTouchEvent方法
ViewGroup特有
返回true,将事件分发到自己的onTouchEvent方法
返回false,将事件分发到下一级View的dispatchTouchEvent
不管返回值是什么,都不会消费事件,只起到分发作用

ACTION_UP、ACTION_MOVE的分发

下面我们来看一下,ACTION_UP事件是怎么分发的,跟ACTION_DOWN有哪些不同。日志中,特意加上了事件的ACTION,枚举如源码所示:

默认返回值

上一节我们知道,默认情况下,ACTION_DOWN是沿着三个方法组成的U型链进行分发。那默认情况下,ACTION_UP的分发,如日志所示:

可以看到,ACTION_UP是没有像ACTION_DOWN一样沿着U型链分发的,它最终只走了Activity的两个方法。用流程图可以清晰地表示出来:

为什么ACTION_UP不跟ACTION_DOWN一样,沿着U型链走一圈呢?这里直接给结论:ACTION_UP的分发路径,取决于ACTION_DOWN事件最终是在哪里被消费的。我们可以试着改变ACTION_DOWN事件的消费位置,来验证这个结论。

改变dispatchTouchEvent返回值

首先将ViewGroup2的dispatchTouchEvent方法返回true,观察日志:

红色是ACTION_DOWN的分发,绿色是ACTION_UP的分发。

流程图:

可以看到ACTION_DOWN事件在ViewGroup2的dispatchTouchEvent被消费了,ACTION_UP跟ACTION_DOWN的分发路径一致,同样也是在ViewGroup2的dispatchTouchEvent被消费了。

改变onTouchEvent返回值

下面我们将ViewGroup2的onTouchEvent方法返回true,观察日志:

流程图:

可以看到,由于ACTION_DOWN在ViewGroup2的onTouchEvent处被消费了,所以ACTION_UP也在ViewGroup2的onTouchEvent处被消费。但ACTION_UP的分发路径有所不同,相比ACTION_DOWN的分发路径相当于抄了一条近路。因为已经知道ACTION_DOWN“路过”View时没有被消费,所以ACTION_UP就不用再次走View了,而是直接从自己的dispatchTouchEvent分发到自己的onTouchEvent处。
如果将ViewGroup2的onInterceptTouchEvent事件也返回true,我们可以预知ACTION_UP的路径跟上面的结果是一样的。这里需要注意一点:这里都没有走ViewGroup2的onInterceptTouchEvent事件,但ViewGroup1的onInterceptTouchEvent事件是有走到的。 所以同样有一个结论:当ACTION_DOWN事件在ViewGroup的onTouchEvent处被消费了,该ViewGroup的onInterceptTouchEvent是收不到ACTION_UP事件的。

3DOWN事件与UP事件分开拦截

下面讲个复杂些的例子。既然我们可以干涉ACTION_DOWN的分发,那么同样也能干涉ACTION_UP的分发。比如我们现在在ViewGroup2的onTouchEvent方法中返回true消费ACTION_DOWN事件,然后在ViewGroup1的dispatchTouchEvent方法中返回false分发ACTION_UP事件,日志如下:


可以看到,ACTION_UP在dispatchTouchEvent返回false时,表现也是跟ACTION_DOWN事件一样,分发给了上一个View的onTouchEvent方法。这样,原先的ACTION_UP分发路径也就被拦截了,ViewGroup1将收不到ACTION_UP事件。所以我们也能同样得出结论:ACTION_UP事件分发的干涉逻辑跟ACTION_DOWN是一样的。 如果有同学观察仔细的话,可以发现在红框内,VIewGroup2额外收到了两个ACTION_CANCEL事件,这是为什么呢?下面会讲到。
至于ACTION_MOVE事件,通过上面几个ACION_UP例子中的日志,我们可以看出来,ACTION_MOVE事件的分发逻辑,跟ACTION_UP事件的分发逻辑是保持一致的。感兴趣的同学可以自己去试一下。

小结

ACTION_UP的消费位置在不干涉的情况下,与ACTION_DOWN的消费位置一致
ACTION_UP事件分发的干涉逻辑跟ACTION_DOWN是一样的
当ACTION_DOWN事件在ViewGroup的onTouchEvent处被消费了,该ViewGroup的onInterceptTouchEvent是收不到ACTION_UP事件的
ACTION_MOVE事件的分发逻辑与ACTION_UP事件的分发逻辑相同

ACTION_CANCEL的分发

上一节ACTION_UP的例子中,我们有发现ACTION_CANCEL的身影,这里直接用那个例子的日志看一下:

这个例子中我们拦截了ACTION_UP事件,在ViewGroup1的dispatchTouchEvent方法中将ACTION_UP事件直接分发给了ViewGroup1的onTouchEvent事件。而ACTION_UP事件原本应该经过ViewGroup2的dispatchTouchEvent方法与onTouchEvent方法,现在不经过了,恰巧这里(上图黄框)打印出来了ACTION_CANCEL事件。也就意味着:当ACTION_UP事件被上一层View拦截时,未分发到ACTION_UP事件的方法会收到ACTION_CANCEL事件。
我们不妨再做个例子验证一下,将ACTION_DOWN事件在View的onTouchEvent方法消费掉,同时将ACTION_UP事件在ViewGroup2的dispatchTouchEvent返回true。日志如下:

流程图:

可以看到,View的dispatchTouchEvent和onTouchEvent方法,本可以收到ACTION_UP事件。但由于上层View拦截的原因,没有收到,此时它们就会收到ACTION_CANCEL事件。同理,ACTION_MOVE事件被上层容器拦截,子容器也是会收到CANCEL事件的,感兴趣的同学可以自行验证一下。
另外还有几个场景同样会触发ACTION_CENCEL,因为不怎么常见,所以这里只列出来一下,来源网上:

ACTION_DOWN初始化操作中
在子View处理事件的过程中被父View中移除
子View被设置为了PFLAG_CANCEL_NEXT_UP_EVENT标志位时

需要注意的是,手势滑出View的范围并不会触发ACTION_CANCEL,这个过程中即使滑出范围了,仍然会一直触发ACTION_MOVE事件,并最后触发ACTION_UP事件,只是不会响应点击罢了。

OnTouchListener和OnClickListener

我们开发时肯定会遇到OnTouchListener和OnClickListener,那么他们俩跟三大金刚是什么关系呢?我们不妨看下源码中是怎么实现的。定位到View的dispatchTouchEvent方法中的这段代码:

if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event)) {
    result = true;
}

if (!result && onTouchEvent(event)) {
    result = true;
}

在dispatchTouchEvent中,会判断View是否设置了OnTouchListener,如果设置了OnTouchListener,就会直接拦截事件,dispatchTouchEvent方法返回true,调用OnTouchListener的onTouch方法,而不会再触发后续的onTouchEvent方法。
再定位到View的onTouchEvent方法中的这段代码:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
    switch (action) {          
        case MotionEvent.ACTION_UP:
            if (!post(mPerformClick)) {
                performClickInternal();
            }

可以发现是在onTouchEvent方法中,判断了View是否可点击。若可点击且设置了OnClickListener,那么就会调用OnClickListener的onClick方法。
所以我们可以得到一个结论:按优先级排序,OnTouchListener>OnTouchEvent>OnClickListener。若设置了OnTouchListener,则不会触发后面两者。OnClickListener在ACTION_UP后触发。

来自:https://juejin.cn/post/7168445102984003591

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。

相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:
一、面试合集

二、源码解析合集

三、开源框架合集

欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值