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

继上一篇分析,今天我们来接着分析Activity的Touch事件是如何分发传递的。

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

1、Activity的dispatchTouchEvent

首先来看Activity的dispatchTouchEvent方法:

Activity的dispatchTouchEvent

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

Activity的onUserInteraction方法

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

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

PhoneWindow的superDispatchTouchEvent方法

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

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 方法

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

2、ViewGroup的dispatchTouchEvent

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

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

dispatchTouchEvent方法片段1

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

View的onFilterTouchEventForSecurity方法

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

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

首先来看cancelAndClearTouchTargets方法:

cancelAndClearTouchTargets方法

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

接着再来看resetTouchState方法:

resetTouchState方法

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

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

dispatchTouchEvent方法片段2

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

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

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

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

onInterceptTouchEvent方法

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

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

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

requestDisallowInterceptTouchEvent方法

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

resetCancelNextUpFlag方法

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

dispatchTouchEvent方法片段3

该段代码首先是一个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)方法。

canViewReceivePointerEvents方法

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

isTransformedTouchPointInView方法

判断视图有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。

dispatchTransformedTouchEvent方法

该方法是一个非常重要的方法,其主要包括三块内容,结构雷同。而且会发现该方法中代码为一个递归调用,若其子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方法的第四个代码块:

dispatchTouchEvent方法片段4

如果没有找到消费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方法的第五个代码块:

dispatchTouchEvent方法片段5

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

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

3、View的dispatchTouchEvent

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

View的dispatchTouchEvent方法

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

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

setOnTouchListener方法

OnTouchListener监听器如下:

OnTouchListener监听器

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

4、View的onTouchEvent

View的onTouchEvent方法源码如下:

View 的 onTouchEvent 方法

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

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

performClick方法

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

setOnClickListener方法

接着来看checkForLongClick方法的源码:

checkForLongClick方法

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

CheckForLongPress类

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

performLongClick 方法

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

重载的performLongClick 方法

直接调用了performLongClickInternal方法:

performLongClickInternal方法

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

setOnLongClickListener方法

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

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

总结

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

Android事件分发机制简图

事件从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()中进行处理。

Android事件分发机制简图

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

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

Android事件分发机制全图

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

原文发布于微信公众号 - 分享达人秀(ShareExpert)

原文发表时间:2017-04-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏jianhuicode

如何使用MVP+Dagger2+RxJava+Retrofit开发(1)

概述 在2016年5,6月份开始在知乎上看到开发方法,那时候记得是看mvc,mvp,mvvm这三种开发模式区别,后面进一步了解到google在github上开源...

4998
来自专栏Android开发与分享

【Android】数据存储(一) SharedPreferences详解

3877
来自专栏Android机动车

MVP模式的经典封装

说到MVP,大家应该都不陌生了,由于其高度解等等优点,越来越多的项目使用这个设计模式。然而,优点虽在,缺点也不少,其中一个就是类多了很多,而且V与P直接要项目通...

1492
来自专栏Android干货园

Android谈谈封装那些事--BaseActivity和BaseFragment(一)

版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/lyhhj/article/details/53...

5303
来自专栏Android研究院

强大灵活的WebView代理库-PrimWeb

PrimWeb 是一个代理的WebView基于的 Android WebView 和 腾讯 x5 WebView,容易、灵活使用以及功能非常强大的库,提供了 W...

3153
来自专栏项勇

笔记18 | 利用MediaRecorder简单实现录音

2076
来自专栏林冠宏的技术文章

android 布局 使用 viewPager 时,如何解决 和 子页面 长按滑动 冲突问题

使用 viewPager 时,如何解决 和 子页面 长按滑动 冲突问题。 我的问题原型:      这个问题,我相信遇到的人会比较少,我是在 一个 viewPa...

21410
来自专栏Android机器圈

Android数据绑定技术二,企业级开发

PS:上一篇文章写了Databinding的简单使用,写了一个绑定textview的示例,和绑定的一些用法,估计有的人会说,之前的写的好好的,为什么要数据绑定这...

3669
来自专栏技术小黑屋

自定义支持读取XML属性的View

在Android中,添加一个View很简单,简单的你可以简简单单地使用xml和一部分简单的java代码就可以搞定。 比如这样

1132
来自专栏Android干货

安卓开发_数据存储技术_内部存储

2595

扫码关注云+社区

领取腾讯云代金券