Android View 的事件分发机制解析

前言当一个事件发生时(例如触摸屏幕),事件会从根View(通常是Activity的布局中的最顶层View)开始,通过一个特定的路径传递到具体的View,这个过程涉及到三个关键的阶段:事件分发、事件拦截和事件消费。

1. 触摸事件类型

ACTION_DOWN: 用户按下触摸屏幕的事件。
ACTION_UP: 用户抬起手指的事件。
ACTION_MOVE: 用户在屏幕上移动手指的事件。
ACTION_CANCEL: 事件被取消,例如由于系统的其他操作。

ACTION_POINTER_DOWN: 当已经有一个手指按下的情况下,另一个手指按下会产生该事件;ACTION_POINTER_UP: 多个手指同时按下的情况下,抬起其中一个手指会产生该事件;


2. 事件传递流程示例

事件产生:用户触摸屏幕。

事件分发:从根View(例如Activity的根布局)开始调用dispatchTouchEvent。

事件拦截:如果根布局的dispatchTouchEvent返回true,则调用其onInterceptTouchEvent判断是否拦截。如果拦截,则调用onTouchEvent处理;如果不拦截,则继续向下传递。

子View处理:如果根布局不拦截,则根据情况继续传递给子View的dispatchTouchEvent进行处理。

最终处理:最终某个View的onTouchEvent返回true,表示该事件被消费,不再继续传递。如果所有View都未消费该事件,则可能由系统处理(如背景点击)。

 3. 事件分发机制

事件分发主要通过三个方法实现:

-- dispatchTouchEvent(MotionEvent ev)

-- onInterceptTouchEvent(MotionEvent ev)

-- onTouchEvent(MotionEvent ev)

1.1 dispatchTouchEvent(MotionEvent ev)

当一个事件发生时,首先会调用dispatchTouchEvent方法。如果当前View是Activity的顶层布局,那么这个方法首先会被调用。该方法返回一个布尔值,如果返回true,表示当前View消费了该事件,事件流终止;如果返回false,则事件将继续传递给其他可能的父View或子View。

public boolean dispatchTouchEvent(MotionEvent ev) {boolean result = false;if (onInterceptTouchEvent(ev)) {result = onTouchEvent(ev);} else {// 尝试传递事件给子Viewresult = event.dispatchToChild(ev);}return result;
}

1.2 onInterceptTouchEvent(MotionEvent ev)

当dispatchTouchEvent返回false时,会调用onInterceptTouchEvent方法。这个方法允许父View拦截子View的事件。如果这个方法返回true,那么当前View将消费这个事件,不再继续向下传递;如果返回false,则事件将继续传递给子View。默认情况下,大多数View的onInterceptTouchEvent返回false。

public boolean onInterceptTouchEvent(MotionEvent ev) {// 默认不拦截所有点击事件return false;
}


1.3 onTouchEvent(MotionEvent ev)

当事件没有被拦截或者已经被当前View拦截时,会调用onTouchEvent方法。如果这个方法返回true,表示当前View消费了该事件;如果返回false,则事件将继续传递给其他可能的父View或子View。

public boolean onTouchEvent(MotionEvent event) {// 处理触摸事件return true; // 表示消费了事件
}

4、ViewGroup 事件分发源码

public boolean dispatchTouchEvent(MotionEvent ev) {......final int action = ev.getAction();final int actionMasked = action & MotionEvent.ACTION_MASK;// 当有 down 操作,会把之前的target 以及标志位都复位if (actionMasked == MotionEvent.ACTION_DOWN) {cancelAndClearTouchTargets(ev);//清除 FLAG_DISALLOW_INTERCEPT,并且设置 mFirstTouchTarget 为 nullresetTouchState(){if(mFirstTouchTarget!=null){mFirstTouchTarget==null;}mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;......};}final boolean intercepted;//ViewGroup是否拦截事件// mFirstTouchTarget是ViewGroup中处理事件(return true)的子View//如果没有子View处理则mFirstTouchTarget=null,ViewGroup自己处理if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;if (!disallowIntercept) {intercepted = onInterceptTouchEvent(ev);//onInterceptTouchEventev.setAction(action);} else {intercepted = false;//如果子类设置requestDisallowInterceptTouchEvent(true)//ViewGroup将无法拦截MotionEvent.ACTION_DOWN以外的事件}} else {intercepted = true;//actionMasked != MotionEvent.ACTION_DOWN并且没有子View处理事件,则将事件拦截//并且不会再调用onInterceptTouchEvent询问是否拦截}............
}

先看标红的代码,这句话的意思是:当 ACTION_DOWN 事件到来时,或者有子元素处理事件( mFirstTouchTarget != null ),如果子 view 没有调用 requestDisallowInterceptTouchEvent 来阻止 ViewGroup 的拦截,那么 ViewGroup 的 onInterceptTouchEvent 就会被调用,来判断是否是要拦截。所以,当子 View 不让父 View 拦截事件的时候,即使父 View onInterceptTouchEvent 中返回true 也没用了。

这里需要注意的就是:onInterceptTouchEvent 默认返回 false。 当 ACTION_DOWN 事件到来时,此时 mFirstTouchTarget 为 null,此时其实也还未收到子 view requestDisallowInterceptTouchEvent。所以这时候,只要父 view 把 ACTION_DOWN 事件给拦截了,那么子 view 就收不到任何事件消息了。所以,一般在 ACTION_DOWN 的时候,父 view 不作拦截。

当 ACTION_MOVE 事件来临时,满足某些条件,父 view 想拦截的时候,这时候子 view 可以在 dispatchTouchEvent 中 ACTION_DOWN 事件来临的时候,调用 requestDisallowInterceptTouchEvent 就可以避免被父 view 拦截。

FLAG_DISALLOW_INTERCEPT 标记位就是通过子 View requestDisallowInterceptTouchEvent 方法设置的。 具体可参看如下代码。

 @Overridepublic void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {// We're already in this state, assume our ancestors are tooreturn;}if (disallowIntercept) {mGroupFlags |= FLAG_DISALLOW_INTERCEPT;} else {mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;}// Pass it up to our parentif (mParent != null) {mParent.requestDisallowInterceptTouchEvent(disallowIntercept);}}

5、View的事件分发源码

public boolean dispatchTouchEvent(MotionEvent event) {  if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  mOnTouchListener.onTouch(this, event)) {  return true;  } return onTouchEvent(event);  }

上述方法只有以下3个条件都为真,dispatchTouchEvent() 才返回 true;否则执行 onTouchEvent()。

  •  mOnTouchListener != null

  •  (mViewFlags & ENABLED_MASK) == ENABLED

  •  mOnTouchListener.onTouch(this, event)

这也就说明如果调用了 setOnTouchListener 设置了 listener, 就会先调用 onTouch 方法。没有的话才会去调用 onTouchEvent 方法。接下去,我们看 onTouchEvent 源码。

public boolean onTouchEvent(MotionEvent event) {  final int viewFlags = mViewFlags;  if ((viewFlags & ENABLED_MASK) == DISABLED) {  return (((viewFlags & CLICKABLE) == CLICKABLE ||  (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  }  // 如果进行了事件代理,就会被拦截,不会在往下面走了if (mTouchDelegate != null) {  if (mTouchDelegate.onTouchEvent(event)) {  return true;  }  }  // 若该控件可点击,则进入switch判断中if (((viewFlags & CLICKABLE) == CLICKABLE ||  (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  switch (event.getAction()) { // a. 若当前的事件 = 抬起View(主要分析)case MotionEvent.ACTION_UP:  boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  ...// 经过种种判断,此处省略// 执行performClick() ->>分析1performClick();  break;  // b. 若当前的事件 = 按下Viewcase MotionEvent.ACTION_DOWN:  if (mPendingCheckForTap == null) {  mPendingCheckForTap = new CheckForTap();  }  mPrivateFlags |= PREPRESSED;  mHasPerformedLongPress = false;  postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  break;  // c. 若当前的事件 = 结束事件(非人为原因)case MotionEvent.ACTION_CANCEL:  mPrivateFlags &= ~PRESSED;  refreshDrawableState();  removeTapCallback();  break;// d. 若当前的事件 = 滑动Viewcase MotionEvent.ACTION_MOVE:  final int x = (int) event.getX();  final int y = (int) event.getY();  int slop = mTouchSlop;  if ((x < 0 - slop) || (x >= getWidth() + slop) ||  (y < 0 - slop) || (y >= getHeight() + slop)) {  // Outside button  removeTapCallback();  if ((mPrivateFlags & PRESSED) != 0) {  // Remove any future long press/tap checks  removeLongPressCallback();  // Need to switch from pressed to not pressed  mPrivateFlags &= ~PRESSED;  refreshDrawableState();  }  }  break;  }  // 若该控件可点击,就一定返回truereturn true;  }  // 若该控件不可点击,就一定返回falsereturn false;  }/*** 分析1:performClick()*/  public boolean performClick() {  if (mOnClickListener != null) {  playSoundEffect(SoundEffectConstants.CLICK);  mOnClickListener.onClick(this);  return true;  // 只要我们通过setOnClickListener()为控件View注册1个点击事件// 那么就会给mOnClickListener变量赋值(即不为空)// 则会往下回调onClick() & performClick()返回true}  return false;  }

从上面的代码我们可以知道,当手指抬起的时候,也就是处于 MotionEvent.ACTION_UP 时,才会去调用 performClick()。而 performClick 中会调用 onClick  方法。

也就说明了:三者优先级是 onTouch->onTouchEvent->onClick

如果 setOnTouchListener 和 setOnclickListener 一起使用:

  • onTouch 方法 return false: 会执行 onClick 方法; down,move,up 这三个方法都会执行

  • onTouch 方法 return true: 不会执行 onClick 方法; down,move,up 这三个方法都会执行

不建议一起使用,可以将点击事件放在up中.

至此 View 的事件分发机制讲解完毕。

6、滑动冲突解决方法

滑动冲突产生的原因:只要在界面中存在内外两层可以同时滑动,就会产生滑动冲突。如下所示:图1是左右滑动和上下滑动冲突,图二是两个view之间的上下滑动冲突;

 解决方案:根据实际情况,判断到底需要谁去响应滑动事件。

主要解决方式有两种,一种是外部拦截法,一种是内部拦截法。

6.1 外部拦截法:

看标题就应该可以知道,外部拦截法,就是通过父 view 来解决滑动冲突。 因为父 view 肯定属于 ViewGroup,所以父 view 根据自己需要来判断是否需要拦截事件。对于 ViewGroup,有个 onInterceptTouchEvent 方法,再需要拦截的时候,返回 true 即可。

public boolean onInterceptTouchEvent(MotionEvent event) {boolean intercepted=false;int x= (int) event.getX();int y= (int) event.getY();switch (event.getAction()){case MotionEvent.ACTION_DOWN:intercepted=false;//必须不能拦截,否则后续的ACTION_MOME和ACTION_UP事件都会拦截。break;case MotionEvent.ACTION_MOVE:if (父容器需要当前点击事件){intercepted=true;}else {intercepted=false;}break;case MotionEvent.ACTION_UP:intercepted=false;break;default:break;}mLastXIntercept=x;mLastXIntercept=y;return intercepted;}

6.2 内部拦截法:

既然外部拦截法是子 view 主动处理拦截,那么内部拦截法就是需要子 view 来处理滑动冲突的情况。那么子view应该如何处理呢?首先子 view 在 dispatchTouchEvent 方法内部调用 requestDisallowInterceptTouchEvent 不让父 view 拦截事件,然后再 onTouchEvent 方法中处理需要拦截的情况。不拦截的时候返回 false,将事件交还给父 view 处理。

// 子 view    public boolean dispatchTouchEvent(MotionEvent ev) {int x = (int) ev.getX();int y = (int) ev.getY();switch (ev.getAction()) {case MotionEvent.ACTION_DOWN: {getParent().requestDisallowInterceptTouchEvent(true);break;}case MotionEvent.ACTION_MOVE: {int deltaX = x - mLastXIntercept;int deltaY = y - mLastYIntercept;//如果是左右滑动if (Math.abs(deltaX) > Math.abs(deltaY)) {getParent().requestDisallowInterceptTouchEvent(false);}break;}case MotionEvent.ACTION_UP: {getParent().requestDisallowInterceptTouchEvent(false);break;}}mLastXIntercept = x;mLastYIntercept = y;return super.dispatchTouchEvent(ev);}

同时为了避免父 view 消费事件,还需要在 DOWN 事件来临的时候,父 view 不会拦截,否则事件就不会传到子 view 了。

public boolean onInterceptTouchEvent(MotionEvent ev) {int action = ev.getAction();if (action == MotionEvent.ACTION_DOWN) {return false;} else {return true;}}

如果子 view 不处理,  父 view 会再次获得事件的处理权限。

参考:

https://blog.csdn.net/qian520ao/article/details/77429593
https://www.jianshu.com/p/38015afcdb58
https://blog.csdn.net/u010302764/article/details/72636459
https://www.cnblogs.com/huansky/p/9656394.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/8836.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

WPS数据分析000005

目录 一、数据录入技巧 二、一维表 三、填充柄 向下自动填充 自动填充选项 日期填充 星期自定义 自定义序列 1-10000序列 四、智能填充 五、数据有效性 出错警告 输入信息 下拉列表 六、记录单 七、导入数据 ​编辑 八、查找录入 会员功能 Xlookup函数 VL…

【Spring】Spring启示录

目录 前言 一、示例程序 二、OCP开闭原则 三、依赖倒置原则DIP 四、控制反转IOC 总结 前言 在软件开发的世界里&#xff0c;随着项目的增长和需求的变化&#xff0c;如何保持代码的灵活性、可维护性和扩展性成为了每个开发者必须面对的问题。传统的面向过程或基于类的设计…

爬虫基础之爬取某基金网站+数据分析

声明: 本案例仅供学习参考使用&#xff0c;任何不法的活动均与本作者无关 网站:天天基金网(1234567.com.cn) --首批独立基金销售机构-- 东方财富网旗下基金平台! 本案例所需要的模块: 1.requests 2.re(内置) 3.pandas 4.pyecharts 其他均需要 pip install 模块名 爬取步骤: …

set集合

set集合 Set系列集合&#xff1a; 无序&#xff1a;存取顺序不一致 不重复&#xff1a;可以去除重复 无索引&#xff1a;没有带索引的方法&#xff0c;所以不能使用普通for循环遍历&#xff0c;也不能通过索引来获取元素 可以看出set是无序的存和打印的顺序不一样 Set接中的…

借DeepSeek-R1东风,开启创业新机遇

DeepSeek-R1的崛起 DeepSeek-R1的推出引发了广泛关注&#xff0c;在AI领域引起了一阵旋风。作为新一代的智能模型&#xff0c;它在多项任务中表现出了卓越的能力。普通人可以借助这个强大的工具&#xff0c;开启属于自己的创业之路&#xff0c;抓住时代带来的机遇。 内容创作…

项目集成Nacos

文章目录 1.环境搭建1.创建模块 sunrays-common-cloud-nacos-starter2.目录结构3.pom.xml4.自动配置1.NacosAutoConfiguration.java2.spring.factories 5.引入cloud模块通用依赖 2.测试1.创建模块 sunrays-common-cloud-nacos-starter-demo2.目录结构3.pom.xml4.application.ym…

系统安全及应用

一&#xff1a;账号安全控制 1.1 系统账号清理 1.1.1 将非登陆用户的Shell 设置为 /sbin/nologin (设置为这个解释器&#xff0c;禁止用户登陆&#xff09; [rootlocalhost ~]# usermod -s /sbin/nologin zhangsan #将用户zhangsan 的登录解释器 设置为 /sbin/n…

从源码深入理解One-API框架:适配器模式实现LLM接口对接

1. 概述 one-api 是一个开源的 API 框架&#xff0c;基于go语言开发&#xff0c;旨在提供统一的接口调用封装&#xff0c;支持多种 AI 服务平台的集成。通过 Gin 和 GORM 等框架&#xff0c;框架简化了多种 API 服务的调用流程。通过适配器模式实现了与多种 大模型API 服务的集…

[权限提升] 操作系统权限介绍

关注这个专栏的其他相关笔记&#xff1a;[内网安全] 内网渗透 - 学习手册-CSDN博客 权限提升简称提权&#xff0c;顾名思义就是提升自己在目标系统中的权限。现在的操作系统都是多用户操作系统&#xff0c;用户之间都有权限控制&#xff0c;我们通过 Web 漏洞拿到的 Web 进程的…

多模态论文笔记——ViViT

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细解读多模态论文《ViViT: A Video Vision Transformer》&#xff0c;2021由google 提出用于视频处理的视觉 Transformer 模型&#xff0c;在视频多模态领域有…

【深度之眼cs231n第七期】笔记(三十一)

目录 强化学习什么是强化学习&#xff1f;马尔可夫决策过程&#xff08;MDP&#xff09;Q-learning策略梯度SOTA深度强化学习 还剩一点小尾巴&#xff0c;还是把它写完吧。&#xff08;距离我写下前面那行字又过了好几个月了【咸鱼本鱼】&#xff09;&#xff08;汗颜&#xff…

[免费]基于Python的Django博客系统【论文+源码+SQL脚本】

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的基于Python的Django博客系统&#xff0c;分享下哈。 项目视频演示 【免费】基于Python的Django博客系统 Python毕业设计_哔哩哔哩_bilibili 项目介绍 随着互联网技术的飞速发展&#xff0c;信息的传播与…

【Docker】Docker入门了解

文章目录 Docker 的核心概念Docker 常用命令示例&#xff1a;构建一个简单的 C 应用容器1. 创建 C 应用2. 创建 Dockerfile3. 构建镜像4. 运行容器 Docker 优势学习 Docker 的下一步 **一、Docker 是什么&#xff1f;****为什么 C 开发者需要 Docker&#xff1f;** **二、核心概…

如何跨互联网adb连接到远程手机-蓝牙电话集中维护

如何跨互联网adb连接到远程手机-蓝牙电话集中维护 --ADB连接专题 一、前言 随便找一个手机&#xff0c;安装一个App并简单设置一下&#xff0c;就可以跨互联网的ADB连接到这个手机&#xff0c;从而远程操控这个手机做各种操作。你敢相信吗&#xff1f;而这正是本篇想要描述的…

基于java线程池和EasyExcel实现数据异步导入

基于java线程池和EasyExcel实现数据异步导入 2.代码实现 2.1 controller层 PostMapping("import")public void importExcel(MultipartFile file) throws IOException {importService.importExcelAsync(file);}2.2 service层 Resource private SalariesListener sa…

linux asio网络编程理论及实现

最近在B站看了恋恋风辰大佬的asio网络编程&#xff0c;质量非常高。在本章中将对ASIO异步网络编程的整体及一些实现细节进行完整的梳理&#xff0c;用于复习与分享。大佬的博客&#xff1a;恋恋风辰官方博客 Preactor/Reactor模式 在网络编程中&#xff0c;通常根据事件处理的触…

Python爬虫学习第三弹 —— Xpath 页面解析 实现无广百·度

早上好啊&#xff0c;大佬们。上回使用 Beautiful Soup 进行页面解析的内容是不是已经理解得十分透彻了~ 这回我们再来尝试使用另外一种页面解析&#xff0c;来重构上一期里写的那些代码。 讲完Xpath之后&#xff0c;小白兔会带大家解决上期里百度搜索的代码编写&#xff0c;保…

docker安装MySQL8:docker离线安装MySQL、docker在线安装MySQL、MySQL镜像下载、MySQL配置、MySQL命令

一、镜像下载 1、在线下载 在一台能连外网的linux上执行docker镜像拉取命令 docker pull mysql:8.0.41 2、离线包下载 两种方式&#xff1a; 方式一&#xff1a; -&#xff09;在一台能连外网的linux上安装docker执行第一步的命令下载镜像 -&#xff09;导出 # 导出镜…

特权模式docker逃逸

目录 1.环境 2.上线哥斯拉 3.特权模式逃逸 1.判断是否为docker环境 2.判断是否为特权模式 3.挂载宿主机磁盘到docker 4.计划任务反弹shell 1.环境 ubuntu部署一个存在CVE-2017-12615的docker: (ip:192.168.117.147) kali(ip:192.168.117.128) 哥斯拉 2.上线哥斯拉…

Direct2D 极速教程(1) —— 画图形

极速导航 Direct2D 简介创建新项目&#xff1a;001-DrawGraphics弄一个白窗口在窗口上画图 Direct2D 简介 大家在学 WINAPI 的时候的时候有没有想过&#xff0c;怎么在一副窗口上画图呢&#xff1f;大家知道 Windows 系统是 GUI 图形用户界面 系统&#xff0c;以 Graphics 图形…