手把手教你读懂源码,View的Touch事件传递流程详细剖析

之前看到有童鞋在聊面试时经常被问到Android的事件传递问题,不知道从何答起。前面分析过View的加载流程详细剖析、View的绘制流程详细剖析、View事件的注册和接收详细剖析,继上一篇分析,今天我们来接着分析Activity的Touch事件是如何分发传递的。

都知道在Android中的事件主要包括三部分内容:分发事件dispatchTouchEvent、拦截事件onInterceptTouchEvent、消费事件onTouchEvent。这几乎是所有开发者都要面临的问题,无论是解决一些事件冲突问题,还是自定义View,都会或多或少涉及到。由于其独特的重要性,大多数面试的时候也基本会有所涉及,所以很好的掌握View的Touch事件传递显得尤其重要。

1、Activity的dispatchTouchEvent

首先来看Activity的dispatchTouchEvent方法:

如果事件为按下状态,则先调用onUserInteraction方法:

该方法为空,从注释可以知道,当此activity在栈顶时,触屏点击按home、back、menu键等都会触发此方法,一般会用于屏保。

接着调用了getWindow().superDispatchTouchEvent(ev),即PhoneWindow类的superDispatchTouchEvent方法:

简单调用了mDecor.superDispatchTouchEvent(event),即DecorView的superDispatchTouchEvent方法:

我们知道DecorView继承自FrameLayout,FrameLayout又继承了ViewGroup,所以这里就是调用了ViewGroup的dispatchTouchEvent方法。

所以执行getWindow().superDispatchTouchEvent(ev)实际上是执行了ViewGroup.dispatchTouchEvent(event),这样事件就从 Activity 传递到了 ViewGroup。这里后续会接着分析。

这里需要注意的是:

当getWindow().superDispatchTouchEvent(ev)返回true时,即Activity的子View拦截了TouchEvent事件,那么接下来就不会再传递给Activity的onTouchEvent 方法,同时Activity的dispatchTouchEvent方法返回true;

反之返回false时,这个事件就交给Activity的onTouchEvent 方法来处理。

可以看到Activity的onTouchEvent 方法返回了false,也就意味着当getWindow().superDispatchTouchEvent(ev)返回false时,Activity的dispatchTouchEvent方法默认返回false。

2、ViewGroup的dispatchTouchEvent

如果要很好掌握Touch事件处理,这部分要重点学习,而且不同Android版本的实现不一致,本文仍然使用最新的Android 7.1源码,相比之前的源码加入了更多的复杂逻辑操作,但是最基本的流程保持一致。

接下来直接分析ViewGroup的dispatchTouchEvent方法,这个方法代码比较多,就分开几段来做分析,首先来看下面这段源码:

其中第一个if语句主要用于调试可直接忽略,后面的变量handled用于表示是否有view消费了该事件,接着调用了父类View的onFilterTouchEventForSecurity方法来判断是否被其他窗口遮盖,方法具体如下:

如果被其他窗口遮挡,该方法返回false,表示需要过滤触摸事件,就会跳过dispatchTouchEvent方法中的if语句代码,直接退出dispatchTouchEvent方法并返回false,表示没有View消费Touch事件;如果没有被其他窗口遮挡,该方法返回true,进而继续执行if语句里面的代码。

每一个事件都是由一个触摸按下事件,一个触摸抬起事件和N个触摸滑动事件组成的,触摸按下事件就是这里的ACTION_DOWN,其为一系列事件的开端。所以在ACTION_DOWN时进行一些初始化操作,分别调用了cancelAndClearTouchTargets方法和resetTouchState方法,用来清楚掉之前消费Touch事件的View信息,并重置触摸状态。

首先来看cancelAndClearTouchTargets方法:

首先判断目标View,如果存在则进行统一清除操作。如果event为空,则将动作设为ACTION_CANCEL,接着用一个for循环不断向下传递触摸事件,然后再清除所有触摸目标,最后在回收拷贝的对象。

接着再来看resetTouchState方法:

该方法非常简单,就是重置了一些Touch标志位。

然后继续回到dispatchTouchEvent方法,看第二个代码块:

变量intercepted用来标记是否要拦截该Touch事件,true表示拦截,false表示不拦截。

接着一个if判断语句,如果为ACTION_DOWN事件,此时还没有找到消费Touch事件的View,所以mFirstTouchTarget为空;如果为ACTION_MOVE和ACTION_UP事件,当前面的ACTION_DOWN事件找到了消费Touch事件的View则mFirstTouchTarget不为空。这两种情况都可以执行if里面的代码块。

变量disallowIntercept 用来标记是否允许拦截,默认为false,但是可以通过 requestDisallowInterceptTouchEvent方法来重置该变量的值。

如果允许拦截,则调用onInterceptTouchEvent方法,即我们熟知的拦截事件。该方法代码如下:

该方法是ViewGroup中特有的方法,用于表示是否拦截触摸事件。返回为true的话则表示拦截事件,事件不在向子View中分发,若返回false的话,则表示不拦截事件,将继续分发事件。

正常都是返回默认的false,但是一般我们在自定义ViewGroup中会重写该方法,用于拦截事件的分发。当我们在父ViewGroup重写该方法返回为true执行事件拦截的逻辑的时候,可以在子View中通过调用requestDisallowInterceptTouchEvent方法,重新设置父ViewGroup的onInterceptTouchEvent方法为false,不拦截对事件的分发逻辑。

这里也是我们在开发中接触碰到的问题,所以需要好好理解一下,下面为requestDisallowInterceptTouchEvent方法的源码:

拦截事件判断完成后,会接着调用resetCancelNextUpFlag方法来检查当前事件是否被取消。

继续回到dispatchTouchEvent方法,看第三个代码块:

该段代码首先是一个if判断语句,如果事件没有被取消,也没有被拦截,就分发该事件。只有ACTION_DOWN事件才会执行第二个if语句里面的代码,对于ACTION_MOVE和ACTION_UP事件则直接传给消费了ACTION_DOWN事件的目标View。

接着获取该ViewGroup中子View的个数,得到该事件发生的位置,获取子View的list集合preorderedList,再通过for循环倒序遍历当前ViewGroup的所有子视图。

有一点值得注意的是,这里采用了倒序遍历,这是由于preorderedList中的顺序是按照addView或XML布局文件中的顺序来的。如点击的地方有两个子View都包含点击事件的坐标,那么后被添加到布局中的那个子view会先响应事件,即点击的时候最上层的那个组件先去响应该事件。

在for循环中第一个if语句调用了canViewReceivePointerEvents(child)和isTransformedTouchPointInView(x, y, child, null)方法。

该方法用于判断当前视图的状态,只有其正在显示或正在执行动画,才可以接受触摸事件。

判断视图有scrollTo或scrollBy造成的滚动偏移也需要计算在内,并判断触摸点是否在当前子视图内。

从这两个方法可知,如果当前子View可以消费该ACTION_DOWN事件,并且该ACTION_DOWN事件发生的位置在当前子View的范围内,则继续执行将ACTION_DOWN事件分发给它;否则continue判断下一个子View可否接受该ACTION_DOWN事件。

然后代码通过调用getTouchTarget方法去查找当前子View是否在mFirstTouchTarget.next这条target链中的某一个targe中,如果在则返回这个target,否则返回null。紧接着用if判断找到接收Touch事件的子View,即newTouchTarget,既然已经找到则执行break跳出for循环。

如果该子View还没有消费掉该ACTION_DOWN事件,就直接调用dispatchTransformedTouchEvent方法将该ACTION_DOWN事件传递给该子View。

该方法是一个非常重要的方法,其主要包括三块内容,结构雷同。而且会发现该方法中代码为一个递归调用,若其子View是ViewGroup则重复执行ViewGroup的dispatchTouchEvent方法,若其子View是View则执行View的dispatchTouchEvent方法。

从最开始到这里,我们大概分析了一下事件分发流程,通过调用Activity的dispatchTouchEvent方法,事件会首先被派发到最顶级的DecorView也就是ViewGroup,再由ViewGroup递归传递到View的dispatchTouchEvent方法。对于View的dispatchTouchEvent方法,我们后面再做分析。

如果dispatchTransformedTouchEvent方法返回true,则表示子View消费掉该事件。那么就回到dispatchTouchEvent方法继续执行if语句里的代码块,将子View加入到mFirstTouchTarget链表的表头,并且将该表头赋值给newTouchTarget,同时 alreadyDispatchedToNewTouchTarget置为true,说明有子View消费掉了该down事件。

for循环执行完毕后,如果newTouchTarget为null,且 mFirstTouchTarget不为null,即没找到子View来消耗该事件,但为了保存Touch事件的链表不为空,则把newTouchTarget赋值为最早加进mFirstTouchTarget链表的target。

再看dispatchTouchEvent方法的第四个代码块:

如果没有找到消费Touch事件的子View,则直接把当前的ViewGroup当作普通的View看待,把事件传递给自己,即前面分析的dispatchTransformedTouchEvent方法中child为null的情况;如果之前的ACTION_DOWN事件被子View消费掉了,就会直接找到该子View对应的Target,将ACTION_MOVE和ACTION_UP事件传递给它们。

这里需要注意的是,如果intercepted为true,也就是ACTION_MOVE和ACTION_UP事件被拦截了,则cancelChild为true,则会分发一次ACTION_CANCLE事件。

再看dispatchTouchEvent方法的第五个代码块:

如果当前事件是ACTION_CANCLE或ACTION_UP,会调用resetTouchState方法清空Touch状态。

至此,ViewGroup的dispatchTouchEvent方法分析完毕。

3、View的dispatchTouchEvent

在分析ViewGroup的dispatchTouchEvent方法时,里面多处调用了dispatchTransformedTouchEvent方法,最终将事件从ViewGroup传递到 View,那么事件在后续如何传递的,接下来继续分析。

相比较ViewGroup的dispatchTouchEvent方法,View的dispatchTouchEvent方法要简便得多。当View没有被其他窗口遮挡时,判断mOnTouchListener是否为空,即判断该View有没有绑定OnTouchListener监听器。

从源码里面可以找到,mOnTouchListener是通过setOnTouchListener方法来进行绑定的:

OnTouchListener监听器如下:

当前View一旦执行了setOnTouchListener方法,该View的mOnTouchListener就不为空,就会调用OnTouchListener监听器的OnTouch方法。从返回值可以看到,如果重写的OnTouch方法返回true的话,那么result的值就为true,意味着该事件被消费掉了,就不会继续执行后面的onTouchEvent方法了;否则继续执行onTouchEvent方法。

4、View的onTouchEvent

View的onTouchEvent方法源码如下:

该方法代码比较多,但是思路非常清晰。可以从第一个if语句看到,即使View为 disable 状态,其依然可以消耗事件。从后面的if语句可以看到,当 View 的 LongClick 或 Clickable 属性,只要有一个为 true则能消耗事件,执行onClick和onLongClick方法。

其中onClick是在ACTION_UP事件中执行的,onLongClick是在ACTION_DOWN事件中执行的,分别对应performClick和checkForLongClick方法。

上面代码判断mOnClickListener是否为空,即判断该View有没有绑定OnClickListener监听器。如果通过调用setOnClickListener方法绑定了OnClickListener监听器,则调用onClick方法。

接着来看checkForLongClick方法的源码:

由于长按事件比较复杂,需要根据ACTION_DOWN事件开始计时,所以这里新建了一个CheckForLongPress对象,其实际为一个Runnable对象:

run方法中调用了performLongClick 方法,继续追踪:

继续调用了重载的performLongClick 方法:

直接调用了performLongClickInternal方法:

上面代码判断mOnLongClickListener是否为空,即判断该View有没有绑定OnLongClickListener监听器。如果通过调用setOnLongClickListener方法绑定了OnLongClickListener监听器,则调用onLongClick方法。

从以上的代码分析知道,如果在ACTION_DOWN事件中已经执行了onLongClick的话,则mHasPerformedLongPress变量会被置为true,这样在ACTION_UP事件中,就会把onClick的回调remove掉,就不会再执行onClick了。

至此,Touch事件的传递流程分析完毕。

总结

按照上面一步一步分析,流程确实比较复杂,只是便于理解具体如何传递的,最后再把其中的关键流程总结一下。主要有以下几点:

事件从Activity.dispatchTouchEveent()开始传递,只要没有拦截,就会从最上层(ViewGroup)开始一直往下传递,子View通过onTouchEvent()消费事件。如果事件从上往下一直传递到最底层的子View,但是该View并没有消费该事件,那么该事件就会反序往上传递,即从该View传递给自己的ViewGroup,然后再传给更上层的ViewGroup直至传递给Activity.onTouchEvent()。

事件从ViewGroup传递给子View时,其中ViewGroup可以通过onInterceptTouchEvent()方法对事件进行拦截,停止其往下传递,如果拦截(即返回true)后该事件会直接走到该ViewGroup中的onTouchEvent()方法,不会再往下传递给子View。如果从DOWN开始,之后的MOVE、UP都会直接在该ViewGroup.onTouchEvent()中进行处理。

如果子View之前在处理某个事件,但是后续被ViewGroup拦截,那么子View会接收到ACTION_CANCEL。如果View没有消费ACTION_DOWN事件,之后其他的ACTION_MOVE和ACTION_UP等事件都不会传递过来。

OnTouchListener优先于onTouchEvent()对事件进行消费,onLongClick优先于oClick对事件进行消费。

这一块的内容详细分析确实比较麻烦,但是整体疏通以后看起来大体还算比较简单的。如果有疑问,欢迎留言一起相互探讨共同进步。

今天就先分享到这里,后续将推出更多精彩内容,欢迎一起探讨学习进步。

此文章版权为微信公众号分享达人秀(ShareExpert)——鑫鱻所有,若转载请备注出处,特此声明!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容