Android手势&触摸事件的分发和消费机制

在Android 客户端开发过程中,经常会遇到手势事件的处理,本篇博文讲一下本人对touch事件处理机制的一些理解,希望对一些初级开发者有所帮助。

我们知道Android的视图是树形结构,如下图所示为例:

由于PhoneWindow和DecorView我们平时是不会有改动的,也用不到,所以我们只关注能用到的三个类:Activity、ViewGroup、View。Activity中有两个方法和touch事件有关,分别是dispatchTouchEvent和onTouchEvent。ViewGroup中有三个方法和touch事件有关,分别是dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。View中有两个方法和touch事件有关,分别是dispatchTouchEvent和onTouchEvent。

我们先讲一下dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三个方法都是干什么的。

1、  dispatchTouchEvent方法是处理touch事件的入口,Activity、ViewGroup、View 都有这个方法,MotionEvent事件会首先由这个方法进行处理。方法负责MotionEvent的分发,如果其自身消费或者其孩子消费了,那么该方法会返回true,否则该方法会返回false,touch事件会返回到其上一层的dispatchTouchEvent方法,交由上层处理。

2、  onInterceptTouchEvent方法只有ViewGroup才有,也就是说只有容器控件才有这个方法,这个方法是ViewGroup用来拦截一个MotionEvent的,由dispatchTouchEvent调用。如果该方法返回true,也就意味着该ViewGroup拦截该touch事件,touch事件将不会分发给孩子,尤其自身的onTouchEvent方法处理;否则,意味着该ViewGroup不拦截touch事件,如果该ViewGroup有孩子,那么分发给孩子处理,如果没有孩子,则由自身的onTouchEvent方法处理。

3、  onTouchEvent方法是针对不同类型的touch事件来处理具体业务的方法,MotionEvent事件的类型主要有ACTION_DOWN、ACTION_MOVE、ACTION_UP等,这个方法里面根据不同的手势操作来处理UI,如果想让上层知道该touch事件被自己消费了,则返回true。

上面只是简单地说了说三个方法的作用,下面会从原理上面讲述touch事件的传递和消费机制,由于该过程十分复杂,描述清楚应该是不可能的事,所以我只能把自己的理解讲一讲,希望读者去其糟粕取其精。

我们以一个简单的UI层次Activity->ViewGroup->View来讲述从手指按下到抬起这期间安卓源码对touch事件的处理流程。

1、  首先Activity的dispatchTouchEvent捕捉到类型为ACTION_DOWN的手势事件,我们先看一下源码,这段源码较短 。

//Activity的dispatchTouchEvent方法源码

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction()== MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)){
        return true;
    }
    return onTouchEvent(ev);
}

通过源码可以看到,如果是事件类型为ACTION_DOWN,会调用onUserInteraction方法,这里跟touch事件的传递没有关系,不做过多阐述。getWindow().superDispatchTouchEvent(ev)这里会通过Window把touch事件传递给Activity的ViewGroup的dispatchTouchEvent方法,如果getWindow().superDispatchTouchEvent(ev)返回true,那么直接就返回true了,否则会调用Activity的onTouchEvent方法。白话得解释就是如果嵌在Activity的ViewGroup处理该touch事件,那么就不会执行自己的onTouchEvent,相反,如果Activity的ViewGroup没有处理该touch事件,那么就会执行自己的onTouchEvent.

2、  ViewGroup的dispatchTouchEvent收到了touch事件,类型为ACTION_DOWN。下面我们根据源码来分析具体逻辑。我们首先来看这段:

if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {intercepted = onInterceptTouchEvent(ev);ev.setAction(action); // restore action in case it was changed} else {intercepted = false;}
} else {// There are no touch targets and this action is not an initial down// so this view group continues to intercept touches.intercepted = true;
}

这段代码会判断MotionEvent事件类型是不是ACTION_DOWN或者mFirstTouchTarget是否为null,单独判断一下ACTION_DOWN说明了该类型的touch事件的特殊性,了解全流程后就会明白,mFirstTouchTarget这个变量代表了该ViewGroup中目前处理touch事件的View,对于ACTION_DOWN类型的事件,mFirstTouchTarget自然为null。根据if语句的条件判断,onInterceptTouchEvent会被调用,我们来看下onInterceptTouchEvent源码:

public boolean onInterceptTouchEvent(MotionEvent ev) {if (ev.isFromSource(InputDevice.SOURCE_MOUSE)&& ev.getAction() == MotionEvent.ACTION_DOWN&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)&& isOnScrollbarThumb(ev.getX(), ev.getY())) {return true;}return false;
}

可知,一般情况下onInterceptTouchEvent方法会返回false。回到dispatchTouchEvent方法,可知道intercepted=false。之后的逻辑就是ViewGroup会建立一个孩子列表,这个孩子列表的顺序是从前台到后台的,根据Z轴的值(android屏幕绘图其实是以三维坐标系为坐标轴的,一些图像的立体显示就是通过Z轴值得变化来实现的)来判断孩子的图层的上下关系。ViewGroup拿到这个孩子列表后,会通过for循环,依次取出一个孩子,调用该孩子的dispatchTouchEvent方法,直到某一个孩子的dispatchTouchEvent返回true,循环停止,mFirstTouchTarget指向该孩子。如果完全按照源码的流程,View的dispatchTouchEvent也会返回false的。

3、  我们来看下View的dispatchTouchEvent的,ViewGroup是View的子类,它重写了该方法,所以ViewGroup和View的dispatchTouchEvent方法的处理逻辑是不一样的,只不过在某些情况下ViewGroup会super.disapatchTouchEvent。

public boolean dispatchTouchEvent(MotionEvent event) {// If the event should be handled by accessibility focus first.if (event.isTargetAccessibilityFocus()) {// We don't have focus or no virtual descendant has it, do not handle the event.if (!isAccessibilityFocusedViewOrHost()) {return false;}// We have focus and got the event, then use normal event dispatch.event.setTargetAccessibilityFocus(false);}boolean result = false;if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesturestopNestedScroll();}if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}if (!result && onTouchEvent(event)) {result = true;}}if (!result && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);}// Clean up after nested scrolls if this is the end of a gesture;// also cancel it if we tried an ACTION_DOWN but we didn't want the rest// of the gesture.if (actionMasked == MotionEvent.ACTION_UP ||actionMasked == MotionEvent.ACTION_CANCEL ||(actionMasked == MotionEvent.ACTION_DOWN && !result)) {stopNestedScroll();}return result;
}

从代码可知,View的dispatchTouchEvent会调用自身注册的OnTouchListener的onTouch方法,如果返回true,则不会再调用onTouchEvent,否则会调用onTouchEvent。View的onTouchEvent方法会判断该View是否可以点击:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {

默认情况下clickable为false,只有在主动调用setClickable(true)或者setOnclickListner方法后,该变量会为true,那么View会执行onclick方法,到这里读者应该知道了我们平时为View设置的onClick方法是什么被调用的,这种情况下View的onTouchEvent会返回true。我们还是不考虑任何人设情况,一律走原生的代码,那么View的onTouchEvent还是返回false。

4、  我们再回到ViewGroup的dispatchTouchEvent方法。在上面3的结尾处说道ViewGroup会依次调用孩子的dispatchTouchEvent方法,默认情况下,当然是所有的孩子都会返回false,那么最后ViewGroup的dispatchTouchEvent自然的毫无意外地也只能返回false,返回值被Activity的dispatchTouchEvent收到。根据1的描述,Activity的dispatchTouchEvent会代用自己的onTouchEvent事件。到这里ACTION_DOWN类型事件算是处理完毕了,传递了一圈,由于没有孩子来消费,最后只能自己消费了。

5、  这一节作为一个补充,仔细想想还是补充一下Activity和ViewGroup之间经过了哪些环节,由1可知Activity的dispatchTouchEvent会调用getWindow().superDispatchTouchEvent(ev),getWindow()获取到的其实是PhoneWindow实例,PhoneWIndow的superDispatchTouchEvent(ev)方法调用是DecorView的dispatchTouchEvent方法,而DecorView是FrameLayout的子类,到这里大家都能明白了吧。

6、  接下来我们来看类型为ACTION_MOVE的touch事件,由Activity的dispatchTouchEvent传到DecorView的dispatchTouchEvent方法,其实调的就是ViewGroup的方法,该方法的逻辑前面已经讲过,DevorView的dispatchTouchEvent方法返回true,并且并没有把touch事件给孩子传递,因为它的mFirstTouchTarget为null。所以最终消费touch事件的还是Activity的onTouchEvent。

7、  ACTION_UP的touch事件的消费过程和7相同,不在赘述。

再次重申一遍,上面的流程是源码的原流程,不考虑经过任何的方法重写,当然也就是最基本的touch事件分发与消费流程了,用这个流程是因为方便讲述。但是实际项目过程中,我们自定义的View,甚至很多Android的封装好的View都已经重写了父类的方法,导致Touch事件的分发与消费流程多种多样,但是只要能弄清楚touch事件的消费的机制,我们就能很好地理解与开发。

这里在总结一下Touch事件消费机制的个人心得:

1、  特别留意ACTION_DOWN事件的处理,因为第一个处理它的View会被标记为target,该target与后续的touch事件处理有很大关系。

2、  Touch事件的传递在视图的树型结构中是按照深度优先遍历方法的,Activity作为根节点,直到某个子View的dispatchTouchEvent为true,停止遍历。如果遍历到直到做后一个叶子节点,都没有View来消费,那么会按照与之前遍历的相反的顺序,调用每个View的onTouchEvent。如下面日志所显示:

3、  ACTION_CANCEL事件我们很少会遇到,但是有时还是能碰到的,这个事件一般是在touch事件传递到target之前,其父View把事件拦截了,那么该target会收到一个ACTION_CANCEL事件,我们明白有这种情况会出现能很好地理解我们实际开发中的一些问题。比如子View对ACTION_DOWN事件消费了,但是其父View重写了onInterceptTouchEvent,对ACTION_MOVE进行了拦截,那么对第一个ACTION_MOVE事件,父View的dispatchTouchEvent会把ACTION_MOVE转化为一个ACTION_CANCEL事件传递给子View,记住父View其实没有处理第一个ACTION_MOVE事件,也就是父View的onTouchEvent方法不会处理第一个ACTION_MOVE事件,虽然对该类型进行了拦截,但是是从第二个ACTION_MOVE开始处理的。

4、  Touch事件传递根据不同情况,具体流程千差万别,所以不可能是读一片文章就能完全明白的,需要通过阅读源码加上自己的实践才能更深入地领会。

Android 系统(74)---Android手势触摸事件的分发和消费机制相关推荐

  1. Android 编程下 Touch 事件的分发和消费机制

    Android 中与 Touch 事件相关的方法包括:dispatchTouchEvent(MotionEvent ev).onInterceptTouchEvent(MotionEvent ev). ...

  2. Android面试必问之触摸事件传递机制

    Android面试必问之触摸事件传递机制 一.Activity的构成 二.触摸事件的类型 三.事件传递的三个阶段 Activity对点击事件的分发过程 五.View的事件分发机制 六.点击事件分发的传 ...

  3. Android 系统(11)---android 系统权限大全

    收集到的android权限都很实用的(permission)大全 1.android.permission.WRITE_USER_DICTIONARY 允许应用程序向用户词典中写入新词 2.andro ...

  4. React Native 手势触摸事件机制详解(进阶篇)

    源码已开源到Github,详细代码可以查看:<React Native 触摸事件代码实践>. 在基础篇,对RN中的触摸事件做了详细的介绍.相信大家对于触摸事件流程机制有了更为清晰的认识.没 ...

  5. Android系统架构-[Android取经之路]

    摘要:本节主要来讲解Android的系统架构 阅读本文大约需要花费10分钟. 文章首发微信公众号:IngresGe 专注于Android系统级源码分析,Android的平台设计,欢迎关注我,谢谢! 欢 ...

  6. android 服务端技术,移动应用服务器端开发(基于JSP技术)-2017 Android系统构架 Android系统构架.docx...

    Android系统构架 PAGE 1 目 录 TOC \o "1-3" \h \z \u 一.Android系统构架 1 二.Linux内核层 2 三.系统运行库层 3 (一)系统 ...

  7. 【android系统】android系统升级流程分析(二)---update升级包分析

    接下来我们将通过几篇文章来分析update.zip包在具体Android系统升级的过程,来理解Android系统中Recovery模式服务的工作原理.今天让我先来分析下升级包update.zip. 一 ...

  8. 【android系统】android系统升级流程分析(一)---recovery模式中进行update包升级流程分析

    今天我们直接来看下android中具体的升级过程是如何的. 升级流程概述 升级的流程图: 升级流程分析 第一步:升级包获取 升级获取可以通过远程下载,也可直接拷贝到指定目录即可. 第二步:准备升级 然 ...

  9. android log抓取方法,Android系统之Android抓取各种log的方法

    Android系统之Android抓取各种log的方法 2018年11月25日 | 萬仟网移动技术 | 我要评论 android之android抓取各种log的方法 1.logcat (四类log b ...

最新文章

  1. c++顺序容器vector用法
  2. Python教程:对 a = [lambda : x for x in range(3)] 的理解
  3. Android深入浅出系列之Android工具的使用—调试桥ADB(二)
  4. [LeetCode] 679. 24 Game(回溯法)
  5. 如何在三层交换机上实现跨VLAN 的DHCP配置
  6. 网络安全:9次实验带你学会网安
  7. 《算法导论3rd第十二章》二叉查找树
  8. 樊登读书会掌控读后感_樊登读书会观后感
  9. LOJ10066 新的开始
  10. 【计科快速入门】五、算术逻辑单元
  11. 精密制造业行业_精密制造业的发展:精密制造业的深度报告
  12. matlab矩阵变成行向量,matlab中将一个矩阵按照行拼成一个行向量应该怎么输?
  13. mPaaS 服务端核心组件:消息推送 MPS 架构及流程设计
  14. 从双非渣硕到字节NLP算法,很强!
  15. lnmp下nginx出现5xx问题解决汇总
  16. 【Database-02】达梦数据库 - DM Manager管理工具安装
  17. python完成文件夹批量word转pdf文件及pdf文件合并+word文件合并
  18. linux日志文件不能清空,定期清空Linux系统日志文件
  19. DataGrip安装和使用
  20. 嵌入式开发难吗?嵌入式多久可以学会?

热门文章

  1. SVD 与 PCA 的直观解释(4): PCA 主成分分析
  2. c语言程序设计i实验8答案,2020中国历史人文地理上尔雅答案
  3. java怎么把毫秒转换成天数_关于java:如何将毫秒转换为“ hh:mm:ss”格式?
  4. BZOJ.4727.[POI2017]Turysta(哈密顿路径/回路 竞赛图)
  5. 2017《时间的朋友》思维导图(脑图整理版)
  6. JQuery操作类数组的工具方法
  7. ios-kvc\kvo 原理
  8. Grunt Server:Fatal error: Port 35729 is already in use by another process.
  9. ntent action大全
  10. 该伙伴事务管理器已经禁止了它对远程/网络事务的支持