0

0

Android View 事件分发机制详解

高洛峰

高洛峰

发布时间:2017-01-16 16:33:33

|

1195人浏览过

|

来源于php中文网

原创

android开发,触控无处不在。对于一些 不咋看源码的同学来说,多少对这块都会有一些疑惑。view事件的分发机制,不仅在做业务需求中会碰到这些问题,在一些面试笔试题中也常有人问,可谓是老生常谈了。我以前也看过很多人写的这方面的文章,不是说的太啰嗦就是太模糊,还有一些在细节上写的也有争议,故再次重新整理一下这块内容,十分钟让你搞明白view事件的分发机制。

说白了这些触控的事件分发机制就是弄清楚三个方法,dispatchTouchEvent(),OnInterceptTouchEvent(),onTouchEvent(),和这三个方法与n个ViewGroup和View堆叠在一起的问题,再复杂的结构都能拆分成1个ViewGroup+1个View。

其实ViewGroup和View都是大同小异,View只是没有了子容器,自然不存在拦截问题,dispatch也很简单,所以弄明白了ViewGroup其实就懂的差不多了。

三个关键方法

public boolean dispatchTouchEvent(MotionEvent ev)

View/ViewGroup处理事件分发的发起者,View/ViewGroup接收到触控事件最先调起的就是这个方法,然后在该方法中判断是否处理拦截或是将事件分发给子容器

public boolean onInterceptTouchEvent(MotionEvent ev)

ViewGroup专用,通过该方法可以达到控件事件的分发方向,一般可以在该方法中判断将事件给ViewGroup独吞或是它继续传递给子容器,是处理事件冲突的最佳地点

public boolean onTouchEvent(MotionEvent event)

触控事件的真正处理者,最后每个事件都会在这里被处理

核心问题

时间分发机制的难点在哪,我觉得难的地方以下几点:三个方法调用规则,确定处理事件的对象以及事件冲突的解决方法。

事件传递规则

一般一次点击会有一系列的MotionEvent,可以简单分为:down->move->….->move->up,当一次event分发到ViewGroup时,上述三个方法之间的 ViewGroup中调用顺序可以用一段简单代码表示

MotionEvent ev;//down or move or up or others...
viewgroup.dispatchTouchEvent(ev);
 
public boolean dispatchTouchEvent(MotionEvent ev){
 boolean isConsumed = false;
  if(onInterceptTouchEvent(ev)){
   isCousumed = this.onTouchEvent(ev);
  }else{
   isConsumed = childView.dispatchTouchEvent(ev);
  }
  return isConsumed;
}

返回结果true表示事件被处理了,返回false表示没有处理。上面的代码通俗易懂,看起来也很简单,一句话就能概括,ViewGroup收到事件后调用dispatch,在dispatch中先检查是否要拦截,若拦截则ViewGroup吃掉事件,否则交给有处理能力的子容器处理。

不过,简单归简单,写成这样只是为了方便理解,ViewGroup的事件处理流程当然没这么简单,这里忽略了很多细节问题,接下来继续补充。回到上面说的,一系列事件我们经常处理的一般都是一个down,多个move和一个up,光靠上面的伪代码是没办法把这些问题都给完美解决,直接来看ViewGroup的dispatchTouchEvent。

onInterceptTouchEvent调用条件

final boolean intercepted;
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;
}

解释一下上面的代码,看起来好像很简单,但真的很简单吗。。在解释之前先说一下intercepted代表的含义,intercepted == false表示父容器ViewGroup暂时不拦截事件,事件有机会传给子View处理,返回true表示父容器直接拦截了该系列事件,后续不会再传递给子View了。子View想获取事件只能让该值为false

onInterceptTouchEvent调用返回false(返回false才能传递给子View,对应到上面伪代码的else中的内容,叫事件传递到子容器需要满足的内容更好理解一些)需要满足两个条件中的任意一个就有可能触发(当然只是有可能):

一个是在down的时候,另一个就是mFirstTouchTarget!=null,那mFirstTouchTarget何时不为空,有兴趣的同学可以看ViewGroup中的addTouchTarget这个方法的调用时机,mFirstTouchTarget就是在这里赋值的,源码太长我就不贴了。

mFirstTouchTarget是用来保存ViewGroup中消费了ACTION_DOWN事件的子View,即在上面伪代码中child.dispatchTouchEvent(ev)在ACTION_DOWN的时候返回true的View,只要有子View的dispatch在ACTION_DOWN返回true,就不会为null(这个赋值过程只发生在ACTION_DOWN里,如果子ViewACTION__DOWN不给它赋值后面序列的事件就不会再),反之,若无子View处理,该对象即为null。当然,满足了上述两个条件还不行,必须还要满足!disallowIntercept。

disallowIntercept这个变量很有意思,它的值主要受FLAG_DISALLOW_INTERCEPT这个标记影响,这个值可以被ViewGroup的子View设置,ViewGroup的子View如果调用了requestDisallowInterceptTouchEvent这个方法,会改变FLAG_DISALLOW_INTERCEPT,导致disallowIntercept这个值就是ture了,这种情况会跳过intercept,导致拦截失效。

但这事还没了,FLAG_DISALLOW_INTERCEPT这个标记有一个重置的机制,查看ViewGroup源码可以看到,在处理MotionEvent.ACTION_DOWN的时候会重置这个标记导致disallowIntercept失效,是不是丧心病狂,上面的一段这么简单的代码有这么多幺蛾子,这里还能得到一个结论,ACTION_DOWN的时候肯定可以执行onInterceptTouchEvent的。

所以拦截的intercepted很重要,能影响到底是让ViewGroup还子View处理这个事件。

上面的两个有可能触发拦截的条件说完了,那么当两个条件都不满足的话就不会再调用拦截了(拦截很重要,一般ViewGroup都返回false这样能把事件传递给子View,如果在ACTION_DOWN时不能走到OnInterceptTouchEvent并返回false告诉ViewGroup不要拦截,则事件再也不能传给子View了,所以拦截一般都是要走到的,而且一般都是返回false这样能让子View有机会处理),这种情况一般都是在ACTION_DOWN处理完之后没有子View当接盘侠消费ACTION_DOWN以及后续事件,从上面的伪代码可以看出来,这时候ViewGroup自己就很被动了,需要自己来调用onTouchEvent来处理,这锅就自己背了。

再继续说一下mFirstTouchTarget和intercepted是怎么影响事件方向的。看源码:

Synthesys
Synthesys

Synthesys是一家领先的AI虚拟媒体平台,用户只需点击几下鼠标就可以制作专业的AI画外音和AI视频

下载
if (!canceled && !intercepted) {
....
if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
 ....
 for(child : childList){
   if(!child satisfied condition....){
     continue;
   }
   newTouchTarget = addTouchTarget(child, idBitsToAssign);//在这里给mFirstTouchTarget赋值
 }
 
 }
}

可以在这里看到intercepted为false在ACTION_DOWN里才能给上面说过的mFirstTouchTarget赋值,只有mFirstTouchTarget不为空才能让后续事件传递给子View,否则根据上上面说的代码后续事件只能给父容器处理了。

mFirstTouchTarget就是我们后续事件传递的对象,很容易理解,如果在ACTION_DOWN中没有确定这个对象,则后续事件不知道传递给谁自然就交给父容器ViewGroup处理了,真正处理事件传递的方法是dispatchTransformedTouchEvent,再看源码:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
      View child, int desiredPointerIdBits) {
   final boolean handled;
 
    // Canceling motions is a special case. We don't need to perform any transformations
    // or filtering. The important part is the action, not the contents.
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
      event.setAction(MotionEvent.ACTION_CANCEL);
      if (child == null) {
        handled = super.dispatchTouchEvent(event);
      } else {
        handled = child.dispatchTouchEvent(event);
      }
      event.setAction(oldAction);
      return handled;
    }
 
}

 

看到没,只要参数里传的child为空,则ViewGroup调用super.dispatchTouchEvent(event),super是谁,ViewGroup继承自View,当然是View咯,View的dispatch调用的谁?当然是自己的onTouchEvent(后面会说),所以这个最后还是调用了ViewGroup自己的onTouchEvent。

那么当child!=null的时候呢,调用的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup则继续按照上面的伪代码执行事件分发,如果也是View则调用自己的onTouchEvent。

所以,说到底事件到底给谁处理,还是和传进来的child有关,那这个方法在哪里调用的呢,继续看:

if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
      } else {
     ...
     dispatchTransformedTouchEvent(ev, cancelChild,
                target.child, target.pointerIdBits)
   }

这就是为什么mFirstTouchTarget能影响事件分发的方向的原因。就这样,整个伪代码的流程是不是很清楚了。

这里需要多说两句,在上上面代码流程中,intercepted决定了这个事件会不会调用ViewGroup的onTouchEvent,当intercepted为true则后续流程会调用ViewGroup的onTouchEvent,仔细看上面的代码能发现,只有两种情况为ture:一是调用了InterceptTouchEvent把事件拦截下来,另一个就是没有一个子View能够消费ActionDown。只有这两种情况父容器ViewGroup才会自己处理
那么问题来了,思考一个问题:如果子View处理了ACTION_DOWN但后续事件都返回false,这些没有被处理的事件最后传给谁处理了?各位思考之,后面再说这个问题。

孩子是谁的

继续来扩展我们的伪代码,拦截条件判断完之后,决定把事件继续传递给子View的时候,会调用childView.dispatchTouchEvent(ev),问题来了,child是哪来的,继续看源码、

if (!canViewReceivePointerEvents(child)
  || !isTransformedTouchPointInView(x, y, child, null)) {
   ev.setTargetAccessibilityFocus(false);
   continue;
}

ViewGroup通过判断所有的子View是否可见是否在播放动画和是否在点击范围内来决定它是否能够有资格接受事件。只有满足条件的child才能够调用dispatch。

再看伪代码,最后dispatch返回ViewGroup的isConsumed,若isConsume == true,说明ViewGroup处理了这个点击事件(ViewGroup自身或者子View处理的),并且这个系列的点击事件会继续传到这个ViewGroup来处理,若isConsume == false(ACTION_DOWN时),ViewGroup没办法处理这个点击事件,那么这个系类的点击事件就和该ViewGroup无缘了。会把这个事件上抛给自己的父容器或者Activity处理。

伪代码说完了,ViewGroup的事件传递规则也就差不多说完了,这么看是不是很简单了。View相对于ViewGroup来说就更简单了,没有拦截方法,dispatch基本上是直接调用了自身的onTouchEvent,处理起来一点难度都木有呀。

一些没说到但也很重要的点

上面解释的东西都很简单,是从一个ViewGroup+一个View开始的,事件分发的执行者是ViewGroup,子容器也只有一个View,但实际开发中当然没这么简单,不过不要怕,再复杂的情况也能够拆分成这种模式的,只不过层次多了一些递归复杂了一些而已,原理还是一样的。

顺带补充几点:

从用户点击屏幕开始触发一个系列的点击事件时,事件真正的传递流程是:Activity(PhoneWindow)->DecorView->ViewGroup->View,在到达ViewGroup之前还有一个DecorView,事件是从Activity传过来的,但这些东西其实和ViewGroup的原理是一样的,Activity能看做一个大的ViewGroup,当它的DecorView包含的所有子View没有人能够消耗事件的时候(这样说有漏洞,大家懂我的意思就行了)最后还是会交给Activity处理。

事件冲突解决可以按照上面的原理在几个point中进行处理。最容易想到的处理的时机是在onInterceptTouchEvent里,比如当一个竖直方向滑动的ViewGroup里嵌套一个横向滑动的ViewGroup,可以在这里的ACTION_MOVE里来判断后续事件应该传递给谁处理,当然,也可以根据上面说的标记位FLAG_DISALLOW_INTERCEPT配合子View的dispatchTouchEvent来控制事件的流向,这都是比较容易想到的,不过看过别的大神,通过分享MotionEvent的方法来控制事件的流向,即在父容器中保存MotionEvent并在适当的时机传入子View自定义的事件处理方法来分享事件,也是可行的。

任何View只要拒绝了一系列事件中的ACTION_DOWN(返回false),则后续事件都不会再传递过来了。但如果拒绝了其他的事件,后续事件还是可以传过来的,比如View某次ACTION_MOVE没处理,这个没处理的事件最后会被Activity消耗掉(而不是View的父容器),但后续的事件还是会继续传给该View。

合理的利用ACTION_CANCEL能够控制一个系列事件的生命周期,让事件处理更加灵活。

理解事件分发的机制只要明白上面的原理基本就够用了,github上很多牛逼的大神写的各种炫酷的自定义控件的事分发根据这些也能够看明白,当然还有很多扩展的东西和更深入的内容由于篇幅的关系在这里就不罗嗦了,更重要的还是去看源码吧。
最后送各位一句经典:纸上得来终觉浅,绝知此事要躬行!

以上就是对Android View事件分发机制的资料整理,后续继续补充相关资料,谢谢大家对本站的支持!

更多Android View 事件分发机制详解相关文章请关注PHP中文网!

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
pixiv网页版官网登录与阅读指南_pixiv官网直达入口与在线访问方法
pixiv网页版官网登录与阅读指南_pixiv官网直达入口与在线访问方法

本专题系统整理pixiv网页版官网入口及登录访问方式,涵盖官网登录页面直达路径、在线阅读入口及快速进入方法说明,帮助用户高效找到pixiv官方网站,实现便捷、安全的网页端浏览与账号登录体验。

286

2026.02.13

微博网页版主页入口与登录指南_官方网页端快速访问方法
微博网页版主页入口与登录指南_官方网页端快速访问方法

本专题系统整理微博网页版官方入口及网页端登录方式,涵盖首页直达地址、账号登录流程与常见访问问题说明,帮助用户快速找到微博官网主页,实现便捷、安全的网页端登录与内容浏览体验。

126

2026.02.13

Flutter跨平台开发与状态管理实战
Flutter跨平台开发与状态管理实战

本专题围绕Flutter框架展开,系统讲解跨平台UI构建原理与状态管理方案。内容涵盖Widget生命周期、路由管理、Provider与Bloc状态管理模式、网络请求封装及性能优化技巧。通过实战项目演示,帮助开发者构建流畅、可维护的跨平台移动应用。

42

2026.02.13

TypeScript工程化开发与Vite构建优化实践
TypeScript工程化开发与Vite构建优化实践

本专题面向前端开发者,深入讲解 TypeScript 类型系统与大型项目结构设计方法,并结合 Vite 构建工具优化前端工程化流程。内容包括模块化设计、类型声明管理、代码分割、热更新原理以及构建性能调优。通过完整项目示例,帮助开发者提升代码可维护性与开发效率。

19

2026.02.13

Redis高可用架构与分布式缓存实战
Redis高可用架构与分布式缓存实战

本专题围绕 Redis 在高并发系统中的应用展开,系统讲解主从复制、哨兵机制、Cluster 集群模式及数据分片原理。内容涵盖缓存穿透与雪崩解决方案、分布式锁实现、热点数据优化及持久化策略。通过真实业务场景演示,帮助开发者构建高可用、可扩展的分布式缓存系统。

23

2026.02.13

c语言 数据类型
c语言 数据类型

本专题整合了c语言数据类型相关内容,阅读专题下面的文章了解更多详细内容。

29

2026.02.12

雨课堂网页版登录入口与使用指南_官方在线教学平台访问方法
雨课堂网页版登录入口与使用指南_官方在线教学平台访问方法

本专题系统整理雨课堂网页版官方入口及在线登录方式,涵盖账号登录流程、官方直连入口及平台访问方法说明,帮助师生用户快速进入雨课堂在线教学平台,实现便捷、高效的课程学习与教学管理体验。

14

2026.02.12

豆包AI网页版入口与智能创作指南_官方在线写作与图片生成使用方法
豆包AI网页版入口与智能创作指南_官方在线写作与图片生成使用方法

本专题汇总豆包AI官方网页版入口及在线使用方式,涵盖智能写作工具、图片生成体验入口和官网登录方法,帮助用户快速直达豆包AI平台,高效完成文本创作与AI生图任务,实现便捷智能创作体验。

421

2026.02.12

PostgreSQL性能优化与索引调优实战
PostgreSQL性能优化与索引调优实战

本专题面向后端开发与数据库工程师,深入讲解 PostgreSQL 查询优化原理与索引机制。内容包括执行计划分析、常见索引类型对比、慢查询优化策略、事务隔离级别以及高并发场景下的性能调优技巧。通过实战案例解析,帮助开发者提升数据库响应速度与系统稳定性。

51

2026.02.12

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Excel 教程
Excel 教程

共162课时 | 17.7万人学习

Java 教程
Java 教程

共578课时 | 67.1万人学习

Uniapp从零开始实现新闻资讯应用
Uniapp从零开始实现新闻资讯应用

共64课时 | 6.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号