最近在看View的事件分发机制,感觉比复杂的地方就是ViewGrop的dispatchTouchEvent函数,便对照着源码研究了一下。故名思意这个函数起到的作用就是分发事件,在具体分析之前还要说明几个相关的知识。

  • 事件序列指的是从手指接触屏幕那一刻起,到手指离开屏幕那一刻为止产生的所有事件。
  • 一旦View消耗了某个事件,那么同一事件序列内的所有事件都会交给它处理。
  • ViewGroup默认不拦截任何事件。
  • 事件分发过程中ViewGroup会考虑多点触控的问题,例如在一个布局中有两个子控件,如果两个手指同时对它们进行操作,控件是可以正常响应的。

现在开始分析该函数的执行过程。(省略了一些不太重要代码)

  1. public boolean dispatchTouchEvent(MotionEvent ev) {
  2. boolean handled = false;
  3. final int action = ev.getAction();
  4. // 去除提供触点信息的pointerIndex字段,可以参考MotionEvent.ACTION_POINTER_INDEX_MASK字段的注释。
  5. final int actionMasked = action & MotionEvent.ACTION_MASK;
  6. if (actionMasked == MotionEvent.ACTION_DOWN) {
  7. // ACTION_DOWN标志着一个事件序列的产生,此时需要进行一些复位操作。
  8. cancelAndClearTouchTargets(ev);
  9. resetTouchState();
  10. }

首先当收到ACTION_DOWN类型的事件时,需要执行一些复位操作,需要执行的两个函数在源码中有如下注释:

Throw away all previous state when starting a new touch gesture.The framework may have dropped the up or cancel event for the previous gesture

due to an app switch, ANR, or some other state change.

新的事件序列产生时需要对状态进行复位,因为在一些情况下系统可能会丢弃标志事件序列结束的ACTION_UP或ACTION_CANCEL事件。

  1. // 标志自身是否拦截此事件。
  2. final boolean intercepted;
  3. if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
  4. // 当子View调用requestDisallowInterceptTouchEvent函数时该变量为true。
  5. final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
  6. if (!disallowIntercept) {
  7. // 如果子View没有禁止父View拦截事件,父View通过该函数判断是否需要拦截此事件。
  8. intercepted = onInterceptTouchEvent(ev);
  9. ev.setAction(action);
  10. } else {
  11. intercepted = false;
  12. }
  13. } else {
  14. // 一定拦截事条件:事件类型不为ACTION_DOWN并且mFirstTouchTarget为null。
  15. // 这说明ACTION_DOWN事件已经被自身消耗,那么该事件序列中的剩余事件也应该被自身消耗。
  16. intercepted = true;
  17. }

接下来是判断ViewGroup是否需要对事件进行拦截。判断拦截条件时需要对成员变量mFirstTouchTarget判空,它是一个链表结构,在一个事件序列中,每当有子View消耗了某个事件,那该View就会被添加到该链表中。此处会与之前的说明有一些歧义,“一旦View消耗了某个事件,那么同一事件序列内的所有事件都会交给它处理”。但是,在事件序列中存在一个比较特殊的事件类型——ACTION_POINTER_DOWN事件,代表有新手指触摸了屏幕,这时新的触点产生的事件可以被另外一个View消耗。在这段代码中还可以看出,即使有子View消耗了一个事件序列中的某些事件(mFirstTouchTarget不为空),父View还是可以拦截之后的事件。

  1. // 判断该事件类型是否应该为ACTION_CANCEL。
  2. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
  3. // 判断一个事件是否可以根据触点的不同向多个View分发,只有这个条件成立时mFirstTouchTarget链表的大小才会大于1。
  4. // 换句话说当条件不成立时多个手指是无法同时操作多个控件的。
  5. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
  6. TouchTarget newTouchTarget = null;
  7. // 标志是否有新的子View(不在mFirstTouchTarget链表中)消耗了事件。
  8. boolean alreadyDispatchedToNewTouchTarget = false;
  9. // 向子View中分发事件的条件:事件类型不为ACTION_CANCEL,ViewGroup本身没有拦截,
  10. // 事件类型为ACTION_DOWN或者当允许同时操作多个控件时有新手指接触屏幕。
  11. if (!canceled && !intercepted) {
  12. if (actionMasked == MotionEvent.ACTION_DOWN
  13. || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)) {

这段代码主要是判断是否需要向子View中分发事件,可以看到当只有允许多个手指同时操作多个控件时,ACTION_POINTER_DOWN类型的事件才会寻找新的子View进行分发,相当于ACTION_POINTER_DOWN类型的事件产生了一个新的事件序列。

  1. // 下边的三行代码比较难理解,在多个手指同时操作对多个控件时起作用。
  2. final int actionIndex = ev.getActionIndex();
  3. // 首先是变量idBitsToAssign,它代表一个bit集合,每个位都与触点Id对应(最大为31)。
  4. // 它的含义这个新的事件序列是由哪个触点产生的。如果不允许多个手指同时操作多个控件,那么
  5. // 它的值为0xFFFFFFFF,即多点触控只会影响一个控件且最多可接收32个触点产生的事件。
  6. final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
  7. // 如果mFirstTouchTarget链表中已有某个View消耗的事件对应的触点Id存在于该集合中,
  8. // 那么该View将不会再收到此触点Id对应的事件,如果该View只接收此触点Id对应的事件,那么该View会被从链表中移除。
  9. removePointersFromTouchTargets(idBitsToAssign);
  10.  
  11. final int childrenCount = mChildrenCount;
  12. if (newTouchTarget == null && childrenCount != 0) {
  13. //获取当前触点的坐标,多点触控时为刚按下的手指对应的触点坐标。
  14. final float x = ev.getX(actionIndex);
  15. final float y = ev.getY(actionIndex);
  16. View[] children = mChildren;
  17. //遍历子View
  18. for (int i = childrenCount - 1; i >= 0; i--) {
  19. int childIndex = i;
  20. final View child = children[childIndex] ;
  21. // 当前View能够接收事件需具备两个条件:
  22. // 1、View可见或正在进行动画效果。
  23. // 2、触点坐标在View内部。
  24. if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
  25. continue;
  26. }

接下来是对子View进行遍历,直到找到满足事件接收条件的子View。

  1. // 满足事件接收条件就会向子View分发事件,在该函数内部会使用函数MotionEvent.split先从原始的事件对象
  2. // 中分离出此触点Id集合中触点所关联的信息。返回true说明子View已消耗了该事件。
  3. if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
  4. mLastTouchDownIndex = childIndex;
  5. mLastTouchDownX = ev.getX();
  6. mLastTouchDownY = ev.getY();
  7. // 将该View与触点Id集合包装后添加到mFirstTouchTarget链表的队首。
  8. newTouchTarget = addTouchTarget(child, idBitsToAssign);
  9. alreadyDispatchedToNewTouchTarget = true;
  10. // 一旦有子View消耗了事件,立即停止遍历。
  11. break;
  12. }
  13. }
  14. }
  15. // 当下面的判断成立时代表此事件为ACTION_POINTER_DOWN并且没有子View消耗此事件。
  16. if (newTouchTarget == null && mFirstTouchTarget != null) {
  17. // 得到消耗第一个触点参数的事件的子View。
  18. newTouchTarget = mFirstTouchTarget;
  19. while (newTouchTarget.next != null) {
  20. newTouchTarget = newTouchTarget.next;
  21. } // 将新的触点Id添加到其触点Id集合中。
  22. newTouchTarget.pointerIdBits |= idBitsToAssign;
  23. }
  24. }
  25. }

当找到符合条件的View后,使用函数dispatchTransformedTouchEvent进行事件分发,实质是调用View的dispatchTouchEvent方法。如果有子View消耗了事件,那么遍历停止,也就是说其余的子View不再有机会接收到此事件序列中的事件。

  1. if (mFirstTouchTarget == null) {
  2. // 如果mFirstTouchTarget为null,代表没有子View可消耗该事件,事件交由本身处理。
  3. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
  4. } else {
  5. TouchTarget predecessor = null;
  6. TouchTarget target = mFirstTouchTarget;
  7. while (target != null) {
  8. final TouchTarget next = target.next;
  9. if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
  10. // 该节点中的View之前已消耗了此事件。
  11. handled = true;
  12. } else {
  13. // 如果该节点中的View将要从父View中移除或父View需要拦截此事件则向该View发送
  14. // 一个ACTION_CANCEL类型的事件。否则从此事件中剥离出该View对应的触点产生的信息并向其发送。
  15. final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
  16. if (dispatchTransformedTouchEvent(ev, cancelChild,
  17. target.child, target.pointerIdBits)) {
  18. handled = true;
  19. }
  20. // 如果一个子View正在接收一个事件序列,里边的事件被父View拦截后,子View无法收这个序列中剩余的事件。
  21. if (cancelChild) {
  22. // 将此节点移出链表。
  23. if (predecessor == null) {
  24. mFirstTouchTarget = next;
  25. } else {
  26. predecessor.next = next;
  27. }
  28. target.recycle();
  29. target = next;
  30. continue;
  31. }
  32. }
  33. predecessor = target;
  34. target = next;
  35. }
  36. }

这代码主要有这么几个功能。第一,如果没有子View消耗当前事件,ViewGroup会把事件交给自身。第二,向已将消耗事件的子View继续分发该事件序列中剩余的事件。第三,在某些情况下需要禁止子View接收当前事件序列中剩余的事件。

  1. if (canceled || actionMasked == MotionEvent.ACTION_UP) {
  2. // 当遇到ACTION_UP类型的事件时标志着一个事件序列的结束,此时需要重置mFirstTouchTarget链表。
  3. resetTouchState();
  4. } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
  5. // 如果有手指离开屏幕(屏幕上还有其它手指),需要查看mFirstTouchTarget链表,移除该触点Id。
  6. final int actionIndex = ev.getActionIndex();
  7. final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
  8. removePointersFromTouchTargets(idBitsToRemove);
  9. }
    }

最后,函数需要处理标志事件序列结束的ACTION_UP类型的事件。如果有手指离开屏幕时(还有手指在上边)并不能算是事件序列的结束,而是要从mFirstTouchTarget链表中移除接收这个手指产生的事件的View。

最后还要说明一个知识点,就是一个MotionEvent对象中会记录多个触点产生的时间,比如两个手指同时按在屏幕上,如果这两个事件分别落在了ViewGroup的两个子View上,那ViewGoup就需要对这个事件进行分解,让每个触点产生的事件信息分发到对应的View上。事件根据触点Id进行分解的代码在ViewGroup的dispatchTransformedTouchEvent函数中,有兴趣的朋友可以自己看一下。

ViewGroup事件分发机制解析的更多相关文章

  1. Android View框架总结(八)ViewGroup事件分发机制

    请尊重分享成果,转载请注明出处: http://blog.csdn.net/hejjunlin/article/details/52298780 上篇分析了View的事件分发流程,留了一个问题:如果上 ...

  2. View的事件分发机制解析

    引言 Android事件构成 在Android中,事件主要包含点按.长按.拖拽.滑动等,点按又包含单击和双击,另外还包含单指操作和多指操作.全部这些都构成了Android中的事件响应.总的来说.全部的 ...

  3. android ViewGroup事件分发机制

    1:事件分销过程 自定义一个LinearLayout,重写dispatchTouchEvent onInterceptTouchEvent onTouchEvent,定义一个按键重写dispathcT ...

  4. Android View 事件分发机制 源码解析 (上)

    一直想写事件分发机制的文章,不管咋样,也得自己研究下事件分发的源码,写出心得~ 首先我们先写个简单的例子来测试View的事件转发的流程~ 1.案例 为了更好的研究View的事件转发,我们自定以一个My ...

  5. Android View 事件分发机制 源代码解析 (上)

    一直想写事件分发机制的文章,无论咋样,也得自己研究下事件分发的源代码.写出心得~ 首先我们先写个简单的样例来測试View的事件转发的流程~ 1.案例 为了更好的研究View的事件转发,我们自定以一个M ...

  6. Android之事件分发机制

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

  7. 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象

    前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...

  8. android view事件分发机制

    首先我们先写个简单的例子来测试View的事件转发的流程~ 1.案例 为了更好的研究View的事件转发,我们自定以一个MyButton继承Button,然后把跟事件传播有关的方法进行复写,然后添加上日志 ...

  9. android菜鸟之路-事件分发机制总结(二)

    ViewGroup事件分发机制 自己定义一个LinearLayout,ImageView和Button,小二,上代码 <LinearLayout xmlns:android="http ...

随机推荐

  1. 深入浅出Hadoop之mapreduce

    卿哥原创,转载请注明出处,谢谢 之前已经作出预告,那么今天就聊聊mapreduce,起源于Google的map reduce paper, 而后经历了mapreduce 1,和构建于yarn上的map ...

  2. 训练 smallcorgi/Faster-RCNN_TF 模型(附ImageNet model百度云下载地址)

    1. 下载训练.验证.测试数据和 VOCdevkit,下载地址: http://host.robots.ox.ac.uk/pascal/VOC/voc2007/VOCtrainval_06-Nov-2 ...

  3. UITableView!别再用代码计算行高了(一)

    你还在用代码去计算行高吗?你不感觉那种方式很low吗?从今天起,试着做些改变吧! 别给我讲你喜欢写代码的感觉,你就是要用代码去计算行高,那我这篇文章不适合你. 在讲解复杂内容之前,还是先学习简单的内容 ...

  4. Jenkins配置备份恢复插件ThinBackup

    一.系统管理-管理插件-找到ThinBackup并安装 二.系统管理-找到ThinBackup-点击Setting进行设置 第一个参数备份目录是必选,其它可选,点保存. 三.保存后返回到ThinBac ...

  5. centos/linux下的使得maven/tomcat能在普通用户是使用

    以下操作#代表在root用户下使用 $表示在普通用户下使用 1.创建新用户 # useradd lonecloud 2.设置该用户的密码 # passwd lonecloud 3.因为昨天将tomca ...

  6. jdbc 报错解决办法

    刚刚看到一个童鞋出现了这个问题 其实这个问题很好解决 在工程中创建一个lib目录: 然后讲mysql包复制进去 然后对着包点击右键 build path就可以了 最后面再次运行就可以了 jar包地址下 ...

  7. 记一次内存溢出的分析经历——thrift带给我的痛orz

    说在前面的话 朋友,你经历过部署好的服务突然内存溢出吗? 你经历过没有看过Java虚拟机,来解决内存溢出的痛苦吗? 你经历过一个BUG,百思不得其解,头发一根一根脱落的烦恼吗? 我知道,你有过! 但是 ...

  8. WPF 照片墙的实现

    主要参照了DevExpress的PhotoGallery实例的实现. 效果如下: 照片墙核心代码如下: PhotoGallery.xaml <local:CarouselDemoModule x ...

  9. basler 相机拍照简单类综合Emgu.CV---得到图档

    在网上找了半天都是下载要钱,自己试做了,经测试能ok,一起分享吧.给初学的人一点鼓励. using System;using System.Collections.Generic;using Syst ...

  10. UVALive - 4329 Ping pong 树状数组

    这题不是一眼题,值得做. 思路: 假设第个选手作为裁判,定义表示在裁判左边的中的能力值小于他的人数,表示裁判右边的中的能力值小于他的人数,那么可以组织场比赛. 那么现在考虑如何求得和数组.根据的定义知 ...