如果要一句话简单总结的话,就是:
    找到一个按照规则“消耗”掉MotionEvent.ACTION_DOWN事件的View,默认情况下,后继会把整个事件流都交给它来处理。
#.总体概括
Android手机是可触屏的设备,其它Android设备一般也是可触屏的。
可触屏设备允许用户与屏幕进行一些触碰的互动,系统识别各式各样的触摸操作,然后做出复杂的功能反应。
本文一切都是针对Android手机来分析说明的。
用户手机触摸屏幕的那一瞬间,Android系统就会把这一次的触屏操作相关信息封装成一个MotionEvent对象,用来描述本次触屏操作,包括触屏操作的类型、发生的时刻、对应的位置坐标等。
每一轮触屏操作,总是以手指按下屏幕开始(ACTION_DOWN),手指在屏幕上可以进行各种轨迹的滑动(ACTION_MOVE),然后手指松开屏幕结束这一轮触屏操作(ACTION_UP)。
在这个过程中,每一次系统识别到了新的触屏操作(按下/滑动/松开),都会封装对应的MotionEvent对象,这一轮的所有事件叫做一个“事件流”。“事流”这个概念在分析事件具体分发的时候会用到。(当然,每个人的叫法不同。)
一个事件流中的事件,会按照View树的结构,分析判断并最终传递给指定的View目标来处理,或者最终没有任何目标View处理,最终交给Activity、Dialog这种最顶层的组件来处理。
正常情况下,一个“事件流”只会交给一个目标View来处理,谁消耗了整个事件流的起始事件——按下事件(MotionEvent.ACTION_DOWN),谁就是目标View,后面整个事件流的所有事件都会逐层传递给它来处理。(之所以说正常情况,是因为开发者可以干预这个过程,通过继承并修改事件传递和拦截的方法,可以在原目标View的更上层ViewGroup拦截事件,将事件流的后继事件交给更上层ViewGroup来处理。)
“按下”(MotionEvent.ACTION_DOWN)事件非常关键,它决定了谁是整个事件流的目标View。
MotionEvent.ACTION_DOWN事件的整个分发过程是按照“责任链模式”来设计的,按照View树结构,从顶层开始先向下一层一层的链式分发,一直分发到View树叶子节点这一层 或者 中间某一层ViewGroup拦截了事件,然后从这一层开始从下往上回溯,回溯的过程中每一层ViewGroup会判断下层子View是否“完成了责任"(即消耗了事件),没消耗的话自己回去尝试“完成责任”,最后通过返回值告诉自己的上一层View:自己这个View树分支是否“完成了责任”。上一层View又会重复这个过程,周而复始,若是没有任何一层来“完成责任”,最后会交给Activity、Dialog这些最顶层的组件来处理。
#.MotionEvent介绍
Android将所有触屏手势事件都封装在了MotionEvent对象中。
1.每种事件有其对应的类型,常见的事件类型有:
        MotionEvent.ACTION_DOWN    //第一个触点被按下时触发(因为可能有多个手指去按屏幕)
        MotionEvent.ACTION_MOVE     //有触点移动时触发
        MotionEvent.ACTION_UP          //最后一个触点放开时触发
        MotionEvent.ACTION_CANCEL  //当前事件流被取消,该类型事件不是由用户操作触发的,而是分发处理逻辑在一定场景下触发的。一般触发场景是:某一个View消耗了MotionEvent.ACTION_DOWN成为该事件流的目标View,但后面按照开发者写的特定拦截逻辑,在上层某个ViewGroup拦截了事件流中的后继事件,那么这时候会触发一次MotionEvent.ACTION_CANCEL事件,传递给原来的目标View。
        MotionEvent.ACTION_POINTER_DOWN    //多个触点时,按下非第一个点时触发
        MotionEvent.ACTION_POINTER_UP    //多个触点时,松开非最后一个点时触

2.可通过getAction()、getActionMasked()获取事件类型   
getAction()返回的值,高8位是触点索引值pointerIndex,低8位是事件类型,对于第一个触点,触点索引值pointerIndex为0。
getActionMasked()返回的只有getAction()返回值中的低8位,对应事件类型。
所以,当只有一个触点时,getAction()与 getActionMasked()返回值相同;当有多个触点时,应该使用getActionMasked()。 

3.获取事件发生位置坐标
1.事件点相对于屏幕的坐标,可通过MotionEvent对象的以下API获取:
getRawX()
    getRawY() 
2.事件点相对于当前View的坐标,可通过MotionEvent对象的以下API获取:
(MotionEvent对象是从上层一级一级传递过来的,当传递到某个View时,可调用相应API获取到相对于该View的位置坐标)
getX()  
    getY()
#.事件分发和处理流程
1.事件分发和处理关联的三个关键方法简介:
1.1public boolean dispatchTouchEvent(MotionEvent event)
由父View来调用,用来分发事件。如果一个View的dispatchTouchEvent()方法被调用,说明事件已经被分发到该View。
返回值ture/false:告诉父View,以子View为根节点的View树分支 是否“消耗”了该事件。
1.2public boolean onInterceptTouchEvent(MotionEvent event) 
只有ViewGroup才有该方法,View类中未定义该方法,因为非ViewGroup的View一般充当叶子节点,无需拦截功能。
判断当前ViewGroup是否拦截事件。如果拦截,自顶而下的事件分发到此为止。而且,只要针对某个事件流中任意事件拦截一次,后继整个事件流都不会再传递到该ViewGroup更下层,其onInterceptTouchEvent()也不会再次调用。
返回值ture/false:当前ViewGroup是否拦截该事件,默认是返回false,即不拦截。
3.public boolean onTouchEvent(MotionEvent event) 
该方法负责针对事件来做对应逻辑处理,即负责“消耗”掉事件。
返回值ture/false:当前View是否消耗了该事件。
2.从Activity等组件传递到View树祖先节点DecorView
当用户进行触屏操作后,经过一系列底层处理,最终会调用Activity或Dialog这些组件的dispatchTouchEvent(MotionEvent event)方法,它们会调用内部Window的dispatchTouchEvent方法,这个Window的具体实现类是PhoneWindow,PhoneWindow又会调用内部顶层View即DecorView的dispatchTouchEvent方法。DecorView继承自FrameLayout,是一个ViewGroup,传递到DecorView,后面就会按照View树结构逐层往下传递。
3.ViewGroup中的分发处理流程
阅读源代码,忽略掉细节(如多个触点相关逻辑),整个分发流程大体可以总结为以下伪代码:
//一个单链表,用来记录当前ViewGroup应该把事件传递给哪个子View
//因为可能有多个触点,所以要用一个链表来记录
private TouchTarget mFirstTouchTarget;
//事件分发方法的大致逻辑
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {boolean handled = false;//是否消费事件if(事件类型 == MotionEvent.ACTION_DOWN){清空之前的各种相关状态和记录,例如清空mFirstTouchTarget中的值,恢复默认状态让ViewGroup不拦截事件;//因为ACTION_DOWN意味着一个新的事件流的开始,需要清除之前的状态,恢复到默认值。//而且一个事件流依靠ACTION_DOWN来选定最终的目标View,//这里清空所有相关状态,恢复默认值,也能保证ACTION_DOWN按需要尽可能往下传递而不被拦截。}/*************    拦截的相关判断和调用      *************/final boolean intercepted;//是否拦截if (事件类型 == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {//如果是ACTION_DOWN时,或者之前已经在ACTION_DOWN时发现了下一层目标View需要判断是否拦截//其中,ACTION_DOWN时,因为前面已经将所有状态都恢复为了默认值,而默认是允许拦截的,所以一定会走到if分支内部if (允许当前ViewGrop拦截) {//允许拦截时,不会代表就会拦截,还要看ViewGroup是否需要拦截//所以要去调用ViewGrup的onInterceptTouchEvent()intercepted = onInterceptTouchEvent(ev);} else {//不允许拦截时,直接设置为falseintercepted = false;}} else {//走到这里,说明没有目标子View,而且是事件流的后继事件(不是第一个事件ACTION_DOWN)//那么一定是需要在本层拦截的,没法交给下一层intercepted = true;}/*************    ACTION_DOWN时去寻找下一层的目标View      *************/boolean canceled = ...判断是否应该取消事件...;//去遍历子孙节点,寻找哪个子View对应的树分支消耗了事件,//那么这个子View将会被添加到mFirstTouchTarget中,mFirstTouchTarget将不再是null//注意:此处的源代码逻辑并非完全如此,这里是伪代码,对源逻辑做了简化概括,而且未考虑多点触控的情况if(!canceled && !intercepted && 事件类型 == MotionEvent.ACTION_DOWN){//只有不取消、不拦截,而且事件类型是MotionEvent.ACTION_DOWN,才会去寻找下一层的目标View//其它情况下,要么for(遍历子View){if(该子View不应该接受事件,例如事件位置在View的范围外等){continue;}//分发事件给子View//该方法内部会调用子View的dispatchTouchEvent()//第二个参数表示是否取消事件流,若为true,则分发给子View的事件类型会被修改成MotionEvent.ACTION_CANCEL//此处第二个参数固定为falseif(dispatchTransformedTouchEvent(ev, false, 子View, ...)){//走到这里,说明子View树消耗了事件修改mFirstTouchTarget,将该子View设置为下一层的目标View;break;}}}/*************      非ACTION_DOWN时的处理      *************///注意:此处的源代码逻辑并非完全如此,这里是伪代码,对源逻辑做了简化概括,而且未考虑多点触控的情况if (mFirstTouchTarget == null) {//如果没有找到下一层的目标View,则交给自己来处理,第三个参数为null时会交给自己来处理handled = dispatchTransformedTouchEvent(ev, canceled, null, ....);} else {//如果找到了下一层的目标View,则传递给已经找到的目标来处理if(当前事件已经在前面的的逻辑流程中被消耗,只有ACTION_DOWN类型且寻找到下一层目标时会如此){handled = true;//标明事件被消耗} else {//如果拦截,则通知子View取消事件流boolean cancelChild = intercepted;//将事件传递给前面已经找到的下一层目标Viewif (dispatchTransformedTouchEvent(ev, cancelChild, target.child, ...)) {//子View树消耗了事件handled = true;}if(cancelChild){清除mFirstTouchTarget的值;//如果ViewGroup拦截,会清除其mFirstTouchTarget的值//所以非ACTION_DOWN事件下次再分发到该ViewGroup时,会直接走到对应判断分支//  设置intercepted=true,并且不会再次调用onInterceptTouchEvent(ev)}}}
}//上面dispatchTransformedTouchEvent()的大致逻辑
//注意:此处的源代码逻辑并非完全如此,这里是伪代码,对源逻辑做了简化概括,而且未考虑多点触控的情况
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {boolean handled = false;//是否消耗事件if(cancel || event是MotionEvent.ACTION_CANCEL类型){将event类型设置为ACTION_CANCEL;if (child == null) {//调用View类的dispatchTouchEvent(),最终内部根据情况能够调用onTouchEvent()//ViewGroup类并未定义自己的onTouchEvent()方法handled = super.dispatchTouchEvent(event);} else {//调用子View的dispatchTouchEvent(event)handled = child.dispatchTouchEvent(event);}return handled;}根据event来新建MotionEvent对象,并调整中间的一些值,例如相对View的偏移坐标等;if (child == null) {//调用View类的dispatchTouchEvent(),最终内部根据情况能够调用onTouchEvent()//ViewGroup类并未定义自己的onTouchEvent()方法handled = super.dispatchTouchEvent(新的MotionEvent对象);} else {//调用子View的dispatchTouchEvent(event)handled = child.dispatchTouchEvent(新的MotionEvent对象);}return handled;
}
4.View类中的分发处理流程
阅读源代码,忽略掉细节,整个分发流程大体可以总结为以下伪代码:
//根据以上分析,如果开发者不覆盖方法重写代码,最终其实会调用到View类的dispatchTouchEvent(event)
//有可能是某个View本身不是ViewGroup类型所以调用到,也可能是在dispatchTransformedTouchEvent()方法中
//  交给ViewGroup来处理事件,它调用了继承自View类的dispatchTouchEvent(event)
//View类的dispatchTouchEvent()大致逻辑
//注意:此处的源代码逻辑并非完全如此,这里是伪代码,对源逻辑做了简化概括
public boolean dispatchTouchEvent(MotionEvent event) {boolean result = false;//是否消耗事件if (View未被禁用 && 事件未被当做滚动事件) {result = true;}if (设置了OnTouchListener && mOnTouchListener.onTouch(this, event))result = true;}if (事件到此还未被消耗 && onTouchEvent(event)) {result = true;}return result;
}public boolean onTouchEvent(MotionEvent event) {//CLICKABLE/LONG_CLICKABLE/CONTEXT_CLICKABLE只要允许任意一项,该值都是trueboolean clickable = ...是否可点击...;if (View被禁用) {//不会处理时间,但只要可点击,会直接返回true,告诉上层已消耗事件return clickable;}if (clickable) {switch (action) {case MotionEvent.ACTION_UP:.......if(可点击并且设置了OnClickListener){执行OnClickListener.onClick(当前View);}.......break;.......}return true;}return false;
}
5.一些补充
ViewGroup提供了public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,
用于设置是否关闭ViewGroup的事件拦截,即传入true时关闭拦截,传入false时允许拦截。
当调用某个ViewGroup的该方法后,会沿着View树不断向上调用父View的requestDisallowInterceptTouchEvent(),开启或者关闭拦截。
#.根据源码中的分发处理流程,可以得到的一些结论和应用场景
##.一些结论
1.一个事件流的ACTION_DOWN事件很关键,在寻找它的消耗者View过程中,每一层ViewGroup都记录了下一层应该把事件传递给谁,于是事件流后面的事件再次分发时,只需要按照之前记录的路径链条,就能分发给消耗掉ACTION_DOWN的View。
2.处理ACTION_DOWN事件时,在ViewGroup.dispatchTouchEvent()中会首先清除前面的相关状态,恢复到默认状态。因此对ViewGroup设置requestDisallowInterceptTouchEvent(true),在下一次事件流到来时会失效,因为默认是允许拦截的。
3.处理非ACTION_DOWN事件时,如果ViewGroup拦截,会清除其mFirstTouchTarget的值,并且向原来的目标View发送一次ACTION_CANCEL类型的事件。
事件流中后面非ACTION_DOWN事件再分发时,分发到该ViewGroup时,会直接走到对应判断分支设置intercepted=true,并且不会再次调用其onInterceptTouchEvent()。无论该ViewGroup每次是否消耗事件,每次都会分发到它这里,因为更上层的mFirstTouchTarget并未被清空,记录着有效的目标。
4.在处理事件时,如果设置了OnTouchListener并且在其onTouch()回调中消耗了事件,将不会再调用View的onTouchEvent()。
5.View的onTouchEvent()中,只要当前View是可点击的一定会返回true,代表着消耗了事件。
但如果当前View被setEnabled(false),即被禁用,不会做任何有效处理,就会按照当前View是否可点击,来直接返回true或false。 
如果View设置了OnClickListener,是经由onTouchEvent()来触发器其对应回调的。
##.应用场景举例
1.解决滑动冲突,例如ViewGroup是上下滑动的,内部View是左右滑动的
一开始由内部View消费ACTION_DOWN,处理事件流。一旦轨迹方向与水平方向超过一定角度,例如45度,就判定为上下滑动,由ViewGroup拦截事件流,负责后继处理。
2.RecyclerView的上拉刷新、下拉刷新动画
一开始由RecyclerView消费ACTION_DOWN,处理事件流。但当滑动到RecyclerView最顶部或最底部还继续滑动时,就由上层ViewGroup拦截事件流,负责后继处理,展示对应的UI效果。

(声明:部分图片获取自网络,这里只是用于学习分享,侵删!)

Android MotionEvent事件分发介绍与流程总结(伪代码形式)相关推荐

  1. Android的事件分发机制

    前言 Android事件分发机制是Android开发者必须了解的基础 网上有大量关于Android事件分发机制的文章,但存在一些问题:内容不全.思路不清晰.无源码分析.简单问题复杂化等等 今天,我将把 ...

  2. 一文读懂Android View事件分发机制

    Android View 虽然不是四大组件,但其并不比四大组件的地位低.而View的核心知识点事件分发机制则是不少刚入门同学的拦路虎.ScrollView嵌套RecyclerView(或者ListVi ...

  3. android 点击事件消费,Android View事件分发和消费源码简单理解

    Android View事件分发和消费源码简单理解 前言: 开发过程中觉得View事件这块是特别烧脑的,看了好久,才自认为看明白.中间上网查了下singwhatiwanna粉丝的读书笔记,有种茅塞顿开 ...

  4. Android之事件分发机制

    本文主要包括以下内容 view的事件分发 viewGroup的事件分发 首先来看两张图 在执行touch事件时 首先执行dispatchTouchEvent方法,执行事件分发. 再执行onInterc ...

  5. Android 系统(218)---Android的事件分发机制以及滑动冲突的解决

    Android的事件分发机制以及滑动冲突的解决 声明:  本文主要涉及VIew的事件分发与滑动冲突的解决,关于View的事件分发流程的部分内容参考自:  Android事件分发机制详解:史上最全面.最 ...

  6. android SDK-25事件分发机制--源码正确解析

    android SDK-25事件分发机制–源码正确解析 Android 事件分发分为View和ViewGroup的事件分发,ViewGroup比View过一个拦截判断,viewgroup可以拦截事件, ...

  7. Android Touch事件分发—拦截—处理

    Android Touch事件分发(dispatchTouchEvent)-拦截(onInterceptTouchEvent)-处理(onTouchEvent) 转自:http://www.cnblo ...

  8. Android的事件分发(处理)

    Android的事件分发:分为View和ViewGroup. View的事件分发,更贴切的可以说是事件处理,毕竟View已经是"最小的"了. 上图: ViewGroup的事件分发, ...

  9. Android触摸事件分发

    Android的触摸分发机制和如何实现拦截 Android的触摸分发机制和如何实现拦截 前言 触摸事件的分发 情景分析 总结 前言 在自定义ViewGroup中,有时候需要实现触摸事件拦截,比如Lis ...

最新文章

  1. 大数据架构师训练营学习笔记
  2. [**奇文共赏**补充问题] 据说看五遍能懂的人智商 200
  3. MikroTik RouterOS x86最大内存只能支持2G
  4. 苹果再遭炮轰;ofo 收购哈罗?华为推可折叠 5G 手机 | 极客头条
  5. 软件测试常问面试题--计算机网络相关
  6. 现在需要在input框输入年月yyyymm的正则_税务师报名时间、考试报名官网2021年安排_税务师...
  7. 模块手机Project Ara的MDK(Module Developers Kit)模块开发套件
  8. C#如何在VS2015 2017版本中编写WPF UI界面引入第三方SVG图形
  9. Six Sigma Basics
  10. android手机内存使用情况分析
  11. 20220727使用汇承科技的蓝牙模块HC-05配对手机进行蓝牙串口的演示
  12. Spring Boot+Spring Security:记住我(Remember-Me): 基于简单加密token的方案 - 第25篇
  13. 关于Fortify 代码安全扫描常见问题
  14. 在openSUSE-Leap-15.2-DVD-x86_64下使用VLC媒体播放器
  15. 部署开源LWM2M服务器 leshan
  16. 软著申请材料,软著申请文件,软著登记材料,软著登记文件
  17. 安装WampServer后无法打开localhost的问题
  18. [转载]ExtJs4 笔记(11) Ext.ListView、Ext.view.View 数据视图
  19. 2.22 ACM模拟赛总结
  20. PADS 找不到FileDir INI文件条目指定的目录

热门文章

  1. php 元组,tuple(元组)
  2. css 容器内 div 底部,Css:div底部的段落
  3. macOS 更改 Terminal 语言
  4. ios 适配iPhonex时可以改变状态栏statusBar的背景颜色
  5. uniapp引入字体
  6. 让人爆笑的国语大片英文译名
  7. JavaBean详解
  8. WinXP DDK的有效下载链接地址
  9. 2023 299的怀旧QQ直播视频直播间搭建 附软件和教程
  10. Vmware中Pnet桥接Cloud