前段时间项目有一个需求,要在点击闪屏的时候做一些处理,刚接到这个需求觉得很简单啊,在原有的view上加个button或者手势识别啥的,后面实现的时候发现还是有点坑。无论我在闪屏上面加button还是手势都无法响应到touch事件,后来也想了很多种可能,比如是否消息传递到了其他视图,可最终发现确是我自己把button从父视图remove的时候把消息也给remove了,具体原因是闪屏显示完成的时候我把button也remove了,而同时显示闪屏的时候项目也做了很多初始化工作,很占用主线程,导致UIApplication sendEvent被阻塞了,当主线程闲置下来的时候,我又把button从父视图remove了,从而一直接收不到touch事件。

  主要也是对事件的整个分发过程不是很了解,查找问题无从下手,因此写下此文对事件的分发做一个详细说明,同时也可以在此基础上做一些拓展。

1.TouchEvents分发流程

  APP的很多页面跳转,视图切换都是通过触摸事件来触发的,比如按钮的点击,view的手势等等,那我们点击屏幕,系统是如何判断我们点击的是哪个view的?答案就是HitTest,下面就给大家介绍下整个流程。

  a)当我们点击设备的屏幕,UIKit就会生成一个事件对象UIEvent,然后会把这个Event分发给当前APP.

  b)当前APP接收到事件后,UIApplication就会去事件队列中取最新的事件,然后通过sendEvent分发给能够处理该事件的对象,但是谁能处理该事件,这个就得靠HitTest来确认了。

  c)HitTest会检测这个点击的点是不是发生在这个View上,如果是,就会去遍历这个View的subviews,直到找到最小的能够处理事件的view,如果找了一遍没找到能够处理的view,则返回自身。当确定了Hit-Test View时,如果当前的application没有忽略触摸事件 (UIApplication:isIgnoringInteractionEvents),则application就会去分发事件(sendEvent:->keywindow:sendEvent:)。

  下面举例来说明下,如下图所示:

  

  步骤:

  1)当我们点击了图中的view 6,因为触摸点在view 1内,所以检查view 1的subview view 2 和view 3

  2)触摸点不在view 2内,触摸点在view 3内,所以检查view 3的subview view 5和view 6

  3)触摸点不在view 5内,触摸点在view 6 内,并且view 6没有subview,所以view 6是view 1中包含这个点的最小单位,所以view 6变成了这次触摸事件的hit-TestView

  说明:

  1)默认的hit-testing顺序是按照UIView中Subviews的逆顺序

  2)如果View的同级别Subview中有重叠的部分,则优先检查顶部的Subview,如果顶部的Subview返回nil, 再检查底部的Subview

  3)Hit-Test也是比较聪明的,检测过程中有这么一点,就是说如果点击没有发生在某View中,那么该事件就不可能发生在View的Subview中,所以检测过程中发现该事件不在ViewB内,也直接就不会检测在不在ViewF内。也就是说,如果你的Subview设置了clipsToBounds=NO,实际显示区域可能超出了superView的frame,你点击超出的部分,是不会处理你的事件的,就是这么任性!

    4)view提供了两个方法来配合HitTest:

   - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

     - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; // default returns YES if point is in bounds

   5)当一个View收到hitTest消息时,会调用自己的pointInside:withEvent:方法,如果pointInside返回YES,则表明触摸事件发生在我自己内部,则会遍历自己的所有Subview去寻找最小单位(没有任何子view)的UIView,如果当前View.userInteractionEnabled = NO,enabled=NO(UIControl),或者alpha<=0.01, hidden等情况的时候,hitTest就不会调用自己的pointInside了,直接返回nil,然后系统就回去遍历兄弟节点,如下代码所示:

  1. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  2. if (self.alpha &lt;= 0.01 || !self.userInteractionEnabled || self.hidden) {
  3. return nil;
  4. }
  5. BOOL inside = [self pointInside:point withEvent:event];
  6. UIView *hitView = nil;
  7. if (inside) {
  8. NSEnumerator *enumerator = [self.subviews reverseObjectEnumerator];
  9. for (UIView *subview in enumerator) {
  10. hitView = [subview hitTest:point withEvent:event];
  11. if (hitView) {
  12. break;
  13. }
  14. }
  15. if (!hitView) {
  16. hitView = self;
  17. }
  18. return hitView;
  19. } else {
  20. return nil;
  21. }
  22. }

  6)hit-Test 是事件分发的第一步,就算你的app忽略了事件,也会发生hit-Test。确定了hit-TestView之后,才会开始进行下一步的事件分发。

2.应用

  我们可以利用hit-Test做一些事情.

  1) 比如我们点击了ViewA,我们想让ViewB响应,这个时候,我们只需要重写View’s hitTest方法,返回ViewB就可以了,虽然可能用不到,但是偶尔还是会用到的。大概代码如下:

  1. @interface STPView : UIView
  2.  
  3. @end
  4.  
  5. @implementation STPView
  6.  
  7. - (instancetype)initWithFrame:(CGRect)frame {
  8. self = [super initWithFrame:frame];
  9. if (self) {
  10. UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
  11. button.frame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) / 2);
  12. button.tag = 10001;
  13. button.backgroundColor = [UIColor grayColor];
  14. [button setTitle:@"Button1" forState:UIControlStateNormal];
  15. [self addSubview:button];
  16. [button addTarget:self action:@selector(_buttonActionFired:) forControlEvents:UIControlEventTouchDown];
  17.  
  18. UIButton *button2 = [UIButton buttonWithType:UIButtonTypeCustom];
  19. button2.frame = CGRectMake(0, CGRectGetHeight(frame) / 2, CGRectGetWidth(frame), CGRectGetHeight(frame) / 2);
  20. button2.tag = 10002;
  21. button2.backgroundColor = [UIColor darkGrayColor];
  22. [button2 setTitle:@"Button2" forState:UIControlStateNormal];
  23. [self addSubview:button2];
  24. [button2 addTarget:self action:@selector(_buttonActionFired:) forControlEvents:UIControlEventTouchDown];
  25. }
  26. return self;
  27. }
  28.  
  29. - (void)_buttonActionFired:(UIButton *)button {
  30. NSLog(@"=====Button Titled %@ ActionFired ", [button titleForState:UIControlStateNormal]);
  31. }
  32.  
  33. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  34. UIView *hitView = [super hitTest:point withEvent:event];
  35. if (hitView == [self viewWithTag:10001]) {
  36. return [self viewWithTag:10002];
  37. }
  38. return hitView;
  39. }
  40.  
  41. @end

  2) 这里给大家提供一个Category,来自STKit,这个category的目的就是方便的编写hitTest方法,由于hitTest方法是override,而不是delegate,所以使用默认的实现方式就比较麻烦。Category如下

  1. /**
  2. * @abstract hitTestBlock
  3. *
  4. * @param 其余参数 参考UIView hitTest:withEvent:
  5. * @param returnSuper 是否返回Super的值。如果*returnSuper=YES,则代表会返回 super hitTest:withEvent:, 否则则按照block的返回值(即使是nil)
  6. *
  7. * @discussion 切记,千万不要在这个block中调用self hitTest:withPoint,否则则会造成递归调用。这个方法就是hitTest:withEvent的一个代替。
  8. */
  9. typedef UIView * (^STHitTestViewBlock)(CGPoint point, UIEvent *event, BOOL *returnSuper);
  10. typedef BOOL (^STPointInsideBlock)(CGPoint point, UIEvent *event, BOOL *returnSuper);
  11.  
  12. @interface UIView (STHitTest)
  13. /// althought this is strong ,but i deal it with copy
  14. @property(nonatomic, strong) STHitTestViewBlock hitTestBlock;
  15. @property(nonatomic, strong) STPointInsideBlock pointInsideBlock;
  16.  
  17. @end
  1. @implementation UIView (STHitTest)
  2.  
  3. const static NSString *STHitTestViewBlockKey = @"STHitTestViewBlockKey";
  4. const static NSString *STPointInsideBlockKey = @"STPointInsideBlockKey";
  5.  
  6. + (void)load {
  7. method_exchangeImplementations(class_getInstanceMethod(self, @selector(hitTest:withEvent:)),
  8. class_getInstanceMethod(self, @selector(st_hitTest:withEvent:)));
  9. method_exchangeImplementations(class_getInstanceMethod(self, @selector(pointInside:withEvent:)),
  10. class_getInstanceMethod(self, @selector(st_pointInside:withEvent:)));
  11. }
  12.  
  13. - (UIView *)st_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  14. NSMutableString *spaces = [NSMutableString stringWithCapacity:20];
  15. UIView *superView = self.superview;
  16. while (superView) {
  17. [spaces appendString:@"----"];
  18. superView = superView.superview;
  19. }
  20. NSLog(@"%@%@:[hitTest:withEvent:]", spaces, NSStringFromClass(self.class));
  21. UIView *deliveredView = nil;
  22. // 如果有hitTestBlock的实现,则调用block
  23. if (self.hitTestBlock) {
  24. BOOL returnSuper = NO;
  25. deliveredView = self.hitTestBlock(point, event, &returnSuper);
  26. if (returnSuper) {
  27. deliveredView = [self st_hitTest:point withEvent:event];
  28. }
  29. } else {
  30. deliveredView = [self st_hitTest:point withEvent:event];
  31. }
  32. // NSLog(@"%@%@:[hitTest:withEvent:] Result:%@", spaces, NSStringFromClass(self.class), NSStringFromClass(deliveredView.class));
  33. return deliveredView;
  34. }
  35.  
  36. - (BOOL)st_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  37. NSMutableString *spaces = [NSMutableString stringWithCapacity:20];
  38. UIView *superView = self.superview;
  39. while (superView) {
  40. [spaces appendString:@"----"];
  41. superView = superView.superview;
  42. }
  43. NSLog(@"%@%@:[pointInside:withEvent:]", spaces, NSStringFromClass(self.class));
  44. BOOL pointInside = NO;
  45. if (self.pointInsideBlock) {
  46. BOOL returnSuper = NO;
  47. pointInside = self.pointInsideBlock(point, event, &returnSuper);
  48. if (returnSuper) {
  49. pointInside = [self st_pointInside:point withEvent:event];
  50. }
  51. } else {
  52. pointInside = [self st_pointInside:point withEvent:event];
  53. }
  54. return pointInside;
  55. }
  56.  
  57. - (void)setHitTestBlock:(STHitTestViewBlock)hitTestBlock {
  58. objc_setAssociatedObject(self, (__bridge const void *)(STHitTestViewBlockKey), hitTestBlock, OBJC_ASSOCIATION_COPY);
  59. }
  60.  
  61. - (STHitTestViewBlock)hitTestBlock {
  62. return objc_getAssociatedObject(self, (__bridge const void *)(STHitTestViewBlockKey));
  63. }
  64.  
  65. - (void)setPointInsideBlock:(STPointInsideBlock)pointInsideBlock {
  66. objc_setAssociatedObject(self, (__bridge const void *)(STPointInsideBlockKey), pointInsideBlock, OBJC_ASSOCIATION_COPY);
  67. }
  68.  
  69. - (STPointInsideBlock)pointInsideBlock {
  70. return objc_getAssociatedObject(self, (__bridge const void *)(STPointInsideBlockKey));
  71. }
  72.  
  73. @end

  代码很简单,就是利用iOS的runtime能力,在hitTest执行之前,插入了一个方法。

  3) iOS7原生的自带NavigationController可以实现从最左侧拖动PopViewController(大约13pt),不管当前可见的ViewController有没有其他的滑动手势或者事件,这是为什么?如何实现。下面简单介绍下:

  如果我们触摸点的坐标 point.x < 13, 我们就让hit-Test 返回NavigationController.view, 把所有的事件入口交给他,否则就返回super,该怎么处理怎么处理,这样就能满足我们的条件,即使当前的VC上面有ScrollView,但是由于点击特定区域的时候,ScrollView根本得不到事件,所以系统会专心处理NavigationController的拖拽手势,而不是ScrollView的事件,当没有点击特定区域的时候,NavigationController的手势不会触发,系统会专心处理ScrollView的事件,互不影响。

  虽然iOS8新增了UIScreenEdgePanGestureRecognizer 手势,但是单纯的用这个手势无法解决当前VC上面有ScrollView的问题。

  当我们确定了HitTestView之后,我们的事件分发就正式开始了,如果hitTestView可以直接处理的,就处理,不能处理的,则交给 The Responder Chain/ GestureRecognizer。

   附上一些测试查找hitTestView过程中打印的日志,可以观察一下:

  1. STPWindow:[hitTest:withEvent:]
  2. ----UIView:[hitTest:withEvent:]
  3. --------STPView:[hitTest:withEvent:]
  4. --------UICollectionView:[hitTest:withEvent:]
  5. ------------UIImageView:[hitTest:withEvent:]
  6. ------------UIImageView:[hitTest:withEvent:]
  7. ------------STDefaultRefreshControl:[hitTest:withEvent:]
  8. ------------STPFeedCell:[hitTest:withEvent:]
  9. ------------STPFeedCell:[hitTest:withEvent:]
  10. ----------------UIView:[hitTest:withEvent:]
  11. --------------------UIImageView:[hitTest:withEvent:]
  12. ------------------------UIImageView:[hitTest:withEvent:]
  13. ------------------------UIView:[hitTest:withEvent:]
  14. ------------------------STImageView:[hitTest:withEvent:]

其中—-表示View的层次结构

参考:http://suenblog.duapp.com/blog/100031/iOS事件分发机制(一)%20hit-Testing

iOS事件分发的更多相关文章

  1. IOS 触摸事件分发机制详解

    欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 作者:MelonTeam 前言 很多时候大家都不关心IOS触摸事件的分发机制的实现原理,当遇到以下几种情形的时候你很可能抓破头皮都找不到解决方案 ...

  2. touch事件分发

    touch事件分发 IOS事件分发 我们知道,如果要一个view(就是view,不是UIControl控件)能够响应事件操作,通常的做法是给该View加上相应的手势,或者重写和touch(当然也可以是 ...

  3. cocos2d-x游戏引擎核心(3.x)----事件分发机制之事件从(android,ios,desktop)系统传到cocos2dx的过程浅析

    (一) Android平台下: cocos2dx 版本3.2,先导入一个android工程,然后看下AndroidManifest.xml <application android:label= ...

  4. iOS学习9_事件分发&amp;响应链

    iOS的三种事件:触摸事件/运动事件/远程控制事件 typedef enum { UIEventTypeTouches, UIEventTypeMotion, UIEventTypeRemoteCon ...

  5. iOS开发事件分发机制—响应链—手势影响

    1.提纲 什么是iOS的事件分发机制 ? 一个事件UIEvent又是如何响应的? 手势对于响应链有何影响? 2.事件分发机制 2.1.来源 以直接触摸事件为例: 当用户一个手指触摸屏幕是会生成一个UI ...

  6. iOS事件传递->处理->响应

    前言: 按照时间顺序,事件的生命周期是这样的: 事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view.寻找最合适的view的底层实现.拦截事件的处理)->找到最合适的view后 ...

  7. Gesture Recognizers与触摸事件分发[转]

    一.Gesture Recognizers Gesture Recognizers是在iOS3.2引入的,可以用来识别手势.简化定制视图事件处理的对象.Gesture Recognizers的基类为U ...

  8. iOS事件响应链

    首先,当发生事件响应时,必须知道由谁来响应事件.在IOS中,由响应者链来对事件进行响应,所有事件响应的类都是UIResponder的子类,响应者链是一个由不同对象组成的层次结构,其中的每个对象将依次获 ...

  9. android自定义控件(9)-Android触摸事件分发机制

    触摸事件的传递机制:   首先是最外层的viewgroup接收到事件,然后调用会调用自己的dispatchTouchEvent方法.如果在ACTION_DOWN的时候dispatchTouchEven ...

随机推荐

  1. AngularJS实战之filter的使用二

    博文一中的filter是angular自带的filter,一般不会满足我们的使用.我们可以自定义filter. 一.自定义filter实现反转字符串 <div>{{ceshi|revers ...

  2. Linux下VNC配置使用总结:开启+桌面配置+安全访问

    操作环境:CentOS 5.3 + Windows XP SP3 32bit + RealVNC 4.1.2 i386 + TigerVNC. 参考:潇湘隐者-Linux系统VNC配置实践总结,萨米的 ...

  3. How to Start a Business in 10 Days

    With an executive staffing venture about to open, a business loan from the in-laws gnawing at her co ...

  4. java重定向与请求转发的区别

    最近工作不算太忙,今天在这里对java中的重定向和请求转发稍作总结,希望能帮助到大家. 请求转发: request.getRequestDispatcher().forward(); 重定向: res ...

  5. Android 响应menu,back键,点击外部消失

    点击外部消失,只需要设置popupWindow.setBackgroundDrawable(new PaintDrawable()); 设置 popupWindow.setFocusable(true ...

  6. 【并查集的另一个思考方向】POJ1456

    POJ1456 这个题一看好像就是用贪心做啊,一个结构体,拍一下序,vis数组一遍遍扫荡,最后输出值,没错,贪心的确能做出来,而这类题目也能应用并查集,实现得思想也是贪心 #include <i ...

  7. axios基础

    一.安装 <script src="https://unpkg.com/axios/dist/axios.min.js"></script> npm ins ...

  8. Delphi7调用DelphiXE编写的DLL问题

    http://bbs.csdn.net/topics/380045353 用DelphiXE在WIN2008下编写一个访问WebServices的DLL ws.dll,只有一个输出函数,如下: fun ...

  9. spring mvc请求过程

    spring mvc处理请求过程 1.    首先客户端发送一个HTTP请求,Web服务器接收这个请求,如果匹配DispatcherServlet的请求映射路径,web容器将请求转交给Dispatch ...

  10. jquery基于form-data文件上传

    1.html代码 <input type="file" name="myupdate" id="myupdate"> 2.jav ...