先介绍下事件分发:
移动平台上的开发主要关注数据以及数据的处理,事件的处理以及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,重写里面的
- - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
- {
- UIView *retView = nil;
- NSLog(@"hitTest %@ Entry! event=%@", self.name, event);
- retView = [super hitTest:point withEvent:event];
- NSLog(@"hitTest %@ Exit! view = %@", self.name, retView);
- return retView;
- }
- - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
- {
- BOOL ret = [super pointInside:point withEvent:event];
- // if ([self.name isEqualToString:@"viewD"]) {
- // ret = YES;
- // }
- if (ret) {
- NSLog(@"pointInside %@ = YES", self.name);
- } else {
- NSLog(@"pointInside %@ = NO", self.name);
- }
- return ret;
- }
在viewDidLoad方法中手动加入5个view,都是myView的实例。
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- _viewA = [[myView alloc] initWithFrame:CGRectMake(10, 10, 300, 200) Color:[UIColor blackColor] andName:@"viewA"];
- [self.view addSubview:_viewA];
- [_viewA release];
- _viewB = [[myView alloc] initWithFrame:CGRectMake(10, 240, 300, 200) Color:[UIColor blackColor] andName:@"viewB"];
- [self.view addSubview:_viewB];
- [_viewB release];
- _viewC = [[myView alloc] initWithFrame:CGRectMake(10, 10, 120, 180) Color:[UIColor blueColor] andName:@"viewC"];
- [_viewB addSubview:_viewC];
- [_viewC release];
- _viewD = [[myView alloc] initWithFrame:CGRectMake(170, 10, 120, 180) Color:[UIColor blueColor] andName:@"viewD"];
- [_viewB addSubview:_viewD];
- [_viewD release];
- _viewE = [[myView alloc] initWithFrame:CGRectMake(30, 40, 60, 100) Color:[UIColor redColor] andName:@"viewE"];
- [_viewD addSubview:_viewE];
- [_viewE release];
- }
这个样式如下:
当我点击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类
- #import <UIKit/UIKit.h>
- extern NSString *const notiScreenTouch;
- @interface DPApplication : UIApplication
- @end
- #import "DPApplication.h"
- NSString *const notiScreenTouch = @"notiScreenTouch";
- @implementation DPApplication
- - (void)sendEvent:(UIEvent *)event
- {
- if (event.type == UIEventTypeTouches) {
- if ([[event.allTouches anyObject] phase] == UITouchPhaseBegan) {
- [[NSNotificationCenter defaultCenter] postNotification:[NSNotification notificationWithName:notiScreenTouch object:nil userInfo:[NSDictionary dictionaryWithObject:event forKey:@"data"]]];
- }
- }
- [super sendEvent:event];
- }
- @end
2.要在main.m文件中替换掉UIApplication的调用
- #import <UIKit/UIKit.h>
- #import "AppDelegate.h"
- #import "DPApplication.h"
- int main(int argc, charchar *argv[])
- {
- @autoreleasepool {
- //return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
- return UIApplicationMain(argc, argv, NSStringFromClass([DPApplication class]), NSStringFromClass([AppDelegate class]));
- }
- }
3. 这时已经实现了拦截消息,并在touchBegan的时候发送Notification,下面就是在view里面注册这个Notification并处理
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onScreenTouch:) name:notiScreenTouch object:nil];
- - (void)onScreenTouch:(NSNotification *)notification
- {
- UIEvent *event=[notification.userInfo objectForKey:@"data"];
- NSLog(@"touch screen!!!!!");
- CGPoint pt = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:self.button];
- NSLog(@"pt.x=%f, pt.y=%f", pt.x, pt.y);
- }
这样就实现了事件的预处理,固有的事件处理机制也没有破坏,这个预处理是静悄悄的进行的。当然,如果我需要把某些事件过滤掉,也只需在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参数,可能是苹果判断单指多指等复杂操作才这样的。