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 plain copy 在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 plain copy 在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 plain copy 在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 plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. #import <UIKit/UIKit.h>  
  2.   
  3. extern NSString *const notiScreenTouch;  
  4.   
  5. @interface DPApplication : UIApplication  
  6.   
  7. @end  

 

 

[objc]  view plain copy 在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 plain copy 在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 plain copy 在CODE上查看代码片 派生到我的代码片
 
  1. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onScreenTouch:) name:notiScreenTouch object:nil];  

 

[objc]  view plain copy 在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参数,可能是苹果判断单指多指等复杂操作才这样的。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自justsee.iteye.com/blog/2202529