`
啸笑天
  • 浏览: 3433587 次
  • 性别: Icon_minigender_1
  • 来自: China
社区版块
存档分类
最新评论

ios 事件拦截

    博客分类:
  • ios
 
阅读更多

先介绍下事件分发:

 移动平台上的开发主要关注数据以及数据的处理,事件的处理以及UI。所以事件的分发处理是很重要的一个环节,对于一个平台的优劣来说也是一项重要的参数。如果事件的分发设计的不好,一些复杂的UI场景就会变得很难写甚至没法写。从小屏没有触摸的功能机开始到现在大屏多点触摸的智能机,对于事件的分发处理基本思路都是一样的——链(设计模式中有个模式就是职责链chain of responsibility),只是判定的复杂程度不同。

        iOS中的事件有3类,触摸事件(单点,多点,手势)、传感器事件(加速度传感器)和远程控制事件,这里我介绍的是第一种事件的分发处理。

        

        上面的这张图来自苹果的官方。描述了Responder的链,同时也是事件处理的顺序。通过这两张图,我们可以发现:

        1. 事件顺着responder chain传递,如果一环不处理,则传递到下一环,如果都没有处理,最后回到UIApplication,再不处理就会抛弃

        2. view的下一级是包含它的viewController,如果没有viewController则是它的superView

        3. viewController的下一级是它的view的superView

        4. view之后是window,最后传给application,这点iOS会比OS X简单(application就一个,window也一个)

         总结出来传递规则是这样的:

        

        这样事件就会从first responder逐级传递过来,直到被处理或者被抛弃。

 

        由于UI的复杂,这个responder chain是需要根据事件来计算的。比如,我现在在一个view内加入了2个Button,先点击了一个,则first responder肯定是这个点击过的button,但我下面可以去点击另一个button,所以显然,当触摸事件来时,这个chain是需要重新计算更新的,这个计算的顺序是事件分发的顺序,基本上是分发的反过来

        

        无论是哪种事件,都是系统本身先获得,是iOS系统来传给UIApplication的,由Application再决定交给谁去处理,所以如果我们要拦截事件,可以在UIApplication层面或者UIWindow层面去拦截。

        

        

        UIView是如何判定这个事件是否是自己应该处理的呢?iOS系统检测到一个触摸操作时会打包一个UIEvent对象,并放入Application的队列,Application从队列中取出事件后交给UIWindow来处理,UIWindow会使用hitTest:withEvent:方法来递归的寻找操作初始点所在的view,这个过程成为hit-test view。

        hitTest:withEvent:方法的处理流程如下:调用当前view的pointInside:withEvent:方法来判定触摸点是否在当前view内部,如果返回NO,则hitTest:withEvent:返回nil;如果返回YES,则向当前view内的subViews发送hitTest:withEvent:消息,所有subView的遍历顺序是从数组的末尾向前遍历,直到有subView返回非空对象或遍历完成。如果有subView返回非空对象,hitTest方法会返回这个对象,如果每个subView返回都是nil,则返回自己。

        好了,我们还是看个例子:

        

        这里ViewA包含ViewB和ViewC,ViewC中继续包含ViewD和ViewE。假设我们点击了viewE区域,则hit-test View判定过程如下:

       1. 触摸在A内部,所以需要检查B和C

       2. 触摸不在B内部,在C内部,所以需要检查D和E

       3. 触摸不在D内部,但在E内部,由于E已经是叶子了,所以判定到此结束

 

        我们可以运行一段代码来验证,首先从UIView继承一个类myView,重写里面的

 

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event  
  2. {  
  3.     UIView *retView = nil;  
  4.     NSLog(@"hitTest %@ Entry! event=%@", self.name, event);  
  5.       
  6.     retView = [super hitTest:point withEvent:event];  
  7.     NSLog(@"hitTest %@ Exit! view = %@", self.name, retView);  
  8.      
  9.     return retView;  
  10. }  

 

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event  
  2. {  
  3.     BOOL ret = [super pointInside:point withEvent:event];  
  4. //    if ([self.name isEqualToString:@"viewD"]) {  
  5. //        ret = YES;  
  6. //    }  
  7.     if (ret) {  
  8.         NSLog(@"pointInside %@ = YES", self.name);  
  9.     } else {  
  10.         NSLog(@"pointInside %@ = NO", self.name);  
  11.     }  
  12.       
  13.     return ret;  
  14. }  

        在viewDidLoad方法中手动加入5个view,都是myView的实例。

 

 

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. - (void)viewDidLoad  
  2. {  
  3.     [super viewDidLoad];  
  4.       
  5.     _viewA = [[myView alloc] initWithFrame:CGRectMake(10, 10, 300, 200) Color:[UIColor blackColor] andName:@"viewA"];  
  6.     [self.view addSubview:_viewA];  
  7.     [_viewA release];  
  8.       
  9.     _viewB = [[myView alloc] initWithFrame:CGRectMake(10, 240, 300, 200) Color:[UIColor blackColor] andName:@"viewB"];  
  10.     [self.view addSubview:_viewB];  
  11.     [_viewB release];  
  12.       
  13.     _viewC = [[myView alloc] initWithFrame:CGRectMake(10, 10, 120, 180) Color:[UIColor blueColor] andName:@"viewC"];  
  14.     [_viewB addSubview:_viewC];  
  15.     [_viewC release];  
  16.       
  17.     _viewD = [[myView alloc] initWithFrame:CGRectMake(170, 10, 120, 180) Color:[UIColor blueColor] andName:@"viewD"];  
  18.     [_viewB addSubview:_viewD];  
  19.     [_viewD release];  
  20.       
  21.     _viewE = [[myView alloc] initWithFrame:CGRectMake(30, 40, 60, 100) Color:[UIColor redColor] andName:@"viewE"];  
  22.     [_viewD addSubview:_viewE];  
  23.     [_viewE release];  
  24.   
  25. }  

        这个样式如下:

 

        当我点击viewE的时候,打印信息如下:

 

2014-01-25 18:32:46.538 eventDemo[1091:c07] hitTest viewB Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(

)}

2014-01-25 18:32:46.538 eventDemo[1091:c07] pointInside viewB = YES

2014-01-25 18:32:46.539 eventDemo[1091:c07] hitTest viewD Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(

)}

2014-01-25 18:32:46.539 eventDemo[1091:c07] pointInside viewD = YES

2014-01-25 18:32:46.539 eventDemo[1091:c07] hitTest viewE Entry! event=<UITouchesEvent: 0x8d0cae0> timestamp: 6671.26 touches: {(

)}

2014-01-25 18:32:46.540 eventDemo[1091:c07] pointInside viewE = YES

2014-01-25 18:32:46.540 eventDemo[1091:c07] hitTest viewE Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>

2014-01-25 18:32:46.540 eventDemo[1091:c07] hitTest viewD Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>

2014-01-25 18:32:46.541 eventDemo[1091:c07] hitTest viewB Exit! view = <myView: 0x8c409f0; frame = (30 40; 60 100); layer = <CALayer: 0x8c40a90>>

2014-01-25 18:32:46.541 eventDemo[1091:c07] touchesBegan viewE

2014-01-25 18:32:46.624 eventDemo[1091:c07] touchesEnded viewE

 

        从打印信息可以看到,先判断了viewB,然后是viewD,最后是viewE,但事件就是直接传给了viewE。

 

 拦截处理方式1:

  我们知道事件的分发是由Application到Window再到各级View的,所以显然最安全可靠的拦截地方是Application。这里拦截事件后如果不手动往下分发,则进入hit-test View过程的机会都没有。

        UIApplication和UIWindow都有sendEvent:方法,用来分发Event。我们可以继承类,重新实现sendEvent:方法,这样就可以拦截下事件,完成一些特殊的处理。

        比如:有一个iPad应用,要求在非某个特定view的区域触摸时进行一项处理。

        我们当然可以在其余每一个view里面增加代码进行判断,不过这样比较累,容易漏掉一些地方;另外当UI需求变更时,维护的GG往往会栽进这个坑,显然这不是一个好方法。

        这里比较简单的解决方案就是在继承UIApplication类,实现自己的sendEvent:,在这个方法里面初步过滤一下事件,是触摸事件就发送Notification,而特定的view会注册这个Notification,收到后判断一下是否触摸到了自己之外的区域。

        恩,还是上代码吧,比较清楚一点:

1. 继承UIApplication的DPApplication类

 

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. #import <UIKit/UIKit.h>  
  2.   
  3. extern NSString *const notiScreenTouch;  
  4.   
  5. @interface DPApplication : UIApplication  
  6.   
  7. @end  

 

 

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. #import "DPApplication.h"  
  2.   
  3. NSString *const notiScreenTouch = @"notiScreenTouch";  
  4.   
  5. @implementation DPApplication  
  6.   
  7. - (void)sendEvent:(UIEvent *)event  
  8. {  
  9.     if (event.type == UIEventTypeTouches) {  
  10.         if ([[event.allTouches anyObject] phase] == UITouchPhaseBegan) {  
  11.             [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:notiScreenTouch object:nil userInfo:[NSDictionary dictionaryWithObject:event forKey:@"data"]]];  
  12.         }  
  13.     }  
  14.     [super sendEvent:event];  
  15. }  
  16.   
  17. @end  

2.要在main.m文件中替换掉UIApplication的调用

 

 

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. #import <UIKit/UIKit.h>  
  2.   
  3. #import "AppDelegate.h"  
  4. #import "DPApplication.h"  
  5.   
  6. int main(int argc, charchar *argv[])  
  7. {  
  8.     @autoreleasepool {  
  9.         //return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));  
  10.         return UIApplicationMain(argc, argv, NSStringFromClass([DPApplication class]), NSStringFromClass([AppDelegate class]));  
  11.     }  
  12. }  

3. 这时已经实现了拦截消息,并在touchBegan的时候发送Notification,下面就是在view里面注册这个Notification并处理

 

 

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onScreenTouch:) name:notiScreenTouch object:nil];  

 

[objc] view plaincopy在CODE上查看代码片派生到我的代码片
 
  1. - (void)onScreenTouch:(NSNotification *)notification  
  2. {  
  3.     UIEvent *event=[notification.userInfo objectForKey:@"data"];  
  4.       
  5.     NSLog(@"touch screen!!!!!");  
  6.     CGPoint pt = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:self.button];  
  7.     NSLog(@"pt.x=%f, pt.y=%f", pt.x, pt.y);  
  8. }  


        这样就实现了事件的预处理,固有的事件处理机制也没有破坏,这个预处理是静悄悄的进行的。当然,如果我需要把某些事件过滤掉,也只需在DPApplication的sendEvent:方法里面抛弃即可。

 

 

 拦截处理方式2:

http://www.cnblogs.com/Quains/p/3369132.html

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

返回在层级上离当前view最远(离用户最近)且包含指定的point的view。

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 返回boolean值指出receiver是否包含指定的point。

重写hittext方法,拦截用户触摸视图的顺序
hitTest方法的都用是由window来负责触发的。
如果希望用户按下屏幕 , 就立刻做出响应 , 使用touchesBegin
如果希望用户离开屏幕 , 就立刻做出响应 , 使用touchesEnd
通常情况下使用touchesBegin,以防止用户认为点击了没有反应。

把hitTest的点转换为 redView的点,使用convertPoint: toView;

 CGPoint redP = [self convertPoint:point toView:self.redView];

判断一个点是否在视图的内部:

if ([self.greenView pointInside:greenP withEvent:event]) {
return self.greenView;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"clcik me root");
}
/*
 重写hittext方法,拦截用户触摸视图的顺序
hitTest方法的都用是由window来负责触发的。
 
 如果希望用户按下屏幕 , 就立刻做出响应 , 使用touchesBegin
 如果希望用户离开屏幕 , 就立刻做出响应 , 使用touchesEnd
 通常情况下使用touchesBegin。
 */

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //1.判断当前视图是否能接受用户响应
    /*self.UserInteractionEnabled=YES
      self.alpha > 0.01;
      self.hidden = no;
     */
    //2.遍历其中的所有的子视图,能否对用户触摸做出相应的响应
    //3.把event交给上级视图活上级视图控制器处理
    //4.return nil;如果发挥nil,说明当前视图及其子视图均不对用户触摸做出反应。
    /*
     参数说明:
        point:参数是用户触摸位置相对于当前视图坐标系的点;
     注视:以下两个是联动使用的,以递归的方式判断具体响应用户事件的子视图
            - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
            - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
        这两个方法仅在拦截触摸事件时使用,他会打断响应者链条,平时不要调用。
     提醒:如果没有万不得已的情况,最好不要自己重写hitTest方法;
     */
    return nil;
    CGPoint redP = [self convertPoint:point toView:self.redView];
    //转换绿色视图的点
    CGPoint greenP = [self convertPoint:point toView:self.greenView];
    //pointInside  使用指定视图中的坐标点来判断是否在视图内部,最好不要在日常开发中都用。
    if ([self.greenView pointInside:greenP withEvent:event]) {
        return self.greenView;
    }
    NSLog(@"%@",NSStringFromCGPoint(redP));
    if ([self.redView pointInside:redP withEvent:event]) {
        
        return self.redView;

    }
    return [super hitTest:point withEvent:event];
}

 

不继承重写的话可以用category来实现:

+(void)initialize{
    Method m = class_getInstanceMethod([UIView class],@selector(pointInside:withEvent:));
    Method m2 = class_getInstanceMethod([UIView class],@selector(pointInside11:withEvent:));
    method_exchangeImplementations(m, m2);
}
- (BOOL)pointInside11:(CGPoint)point withEvent:(UIEvent *)event
{
//todo 
   if (*****) {

        return NO;
    }
    
    return [self pointInside11:point withEvent:event];
}

  

 

不解:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 和- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 方法都执行三遍,不知道具体原理 ,运行下看了event参数,可能是苹果判断单指多指等复杂操作才这样的。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

分享到:
评论
15 楼 啸笑天 2017-03-09  
史上最详细的iOS之事件的传递和响应机制-原理篇
http://www.jianshu.com/p/2e074db792ba
14 楼 啸笑天 2017-03-09  
事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。
13 楼 啸笑天 2017-03-09  
如何做到一个事件多个对象处理:
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
12 楼 啸笑天 2017-03-09  
触摸事件处理的整体过程

1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理
11 楼 啸笑天 2017-03-09  
想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
10 楼 啸笑天 2017-03-09  
iOS事件响应链中Hit-Test View的应用
http://www.jianshu.com/p/d8512dff2b3e

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}


//in custom button .m
//overide this method
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}

CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
    CGRect hitTestingBounds = bounds;
    if (minimumHitTestWidth > bounds.size.width) {
        hitTestingBounds.size.width = minimumHitTestWidth;
        hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
    }
    if (minimumHitTestHeight > bounds.size.height) {
        hitTestingBounds.size.height = minimumHitTestHeight;
        hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
    }
    return hitTestingBounds;
}


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    /**
     *  此注释掉的方法用来判断点击是否在父View Bounds内,
     *  如果不在父view内,就会直接不会去其子View中寻找HitTestView,return 返回
     */
//    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
//    }
    return nil;
}


- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    UIView *hitTestView = [super hitTest:point withEvent:event];
    if (hitTestView) {
        hitTestView = self.scrollView;
    }
    return hitTestView;
}
9 楼 啸笑天 2016-03-03  
Responder Chain简析  http://blog.csdn.net/chun799/article/details/8223612
如果需要将事件传递给next responder,可以直接调用super的对应事件处理方法,super的对应方法将事件传递给next responder,即使用

[super touchesBegan:touches withEvent:event];
不建议直接向nextResponder发送消息,这样可能会漏掉父类对这一事件的其他处理。

[self.nextResponder  touchesBegan:touches withEvent:event];
另外,在定制UIView子类的事件处理方法时,如果其中一个方法没有调用super的对应方法,则其他方法也需要重写,不使用super的方法,否则事件处理流程会很混乱。
8 楼 啸笑天 2016-01-15  
过程应该是 UIView 在接受到 hit-testing 消息后,先是判断自身的 alpha 、userInteractionEnabled、hidden 等属性,如果这些属性不满足要求那么

- (UIView *)hitTest:(CGPoint)*point* withEvent:(UIEvent *)*event;
直接返回 nill ,如果符合要求就掉用

- (BOOL)pointInside:(CGPoint)*point* withEvent:(UIEvent *)*event;
判断事件是否发生在自己这里,如果不在自己这里,就返回 nill ,如果是在自己这里那么就对自己的 SubView 调用

- (UIView *)hitTest:(CGPoint)*point* withEvent:(UIEvent *)*event;
从而得到一个 View 并且返回。
7 楼 啸笑天 2016-01-15  
但是问题来了 hit-testing 是以什么顺序找 SubView 的呢。就是你添加 SubView 的逆序来遍历的,换句话说就是从最顶层的 SubView 开始找。
http://www.jianshu.com/p/c5fee92ddf31
6 楼 啸笑天 2016-01-15  
在遍历子视图时会按照层级从高到低的顺序,一旦有视图可以接受此次事件,就不会再往下遍历。
5 楼 啸笑天 2016-01-15  
手势UIGestureRecognizer对象也可以附加在view上,来实现其他丰富的手势事件。在view添加单击手势之后,原来的touchesEnded方法就无效了。最开始我一直认为view添加手势之后,原有的touches系列方法全部无效。但是在测试demo中,发现view添加手势之后,touchesBegan方法是有进行回调的,但是moved跟ended就没有进行回调。
4 楼 啸笑天 2016-01-15  
事件对象
iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到有一个UIEventType类型的属性,这个属性表示了当前的响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一的
点击对象
UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中,我们通过集合中对象获取用户点击的位置。其中通过- (CGPoint)locationInView:(nullable UIView *)view获取当前点击坐标点,- (CGPoint)previousLocationInView:(nullable UIView *)view获取上个点击位置的坐标点。
3 楼 啸笑天 2016-01-15  
在iOS中,能够响应事件的对象都是UIResponder的子类对象。UIResponder提供了四个用户点击的回调方法,分别对应用户点击开始、移动、点击结束以及取消点击,其中只有在程序强制退出或者来电时,取消点击事件才会调用。
2 楼 啸笑天 2016-01-15  
操作系统把包含这些点击事件的信息包装成UITouch和UIEvent形式的实例,然后找到当前运行的程序,逐级寻找能够响应这个事件的对象,直到没有响应者响应。这一寻找的过程,被称作事件的响应链,如下图所示,不用的响应者以链式的方式寻找
1 楼 啸笑天 2016-01-15  
http://www.jianshu.com/p/a8926633837b 
http://www.2cto.com/kf/201504/388332.html

相关推荐

Global site tag (gtag.js) - Google Analytics