事件传递机制和响应者链条,App使用响应者对象

作者:澳门新葡京平台游戏

最近在写一个图片浏览的需求,一些地方我使用了响应者来处理,顺便又去看看了官方文档,这里记录一下官方文档,并给出一些示例加深理解。

概述

 

iOS中加载的时候会先执行main函数

App使用响应者对象接收和处理事件,响应者对象是任何UIResponder的实例。UIResponder的子类包括UIView,UIViewController,UIApplication等。响应者接收到原始事件数据,必须处理事件或者转发到另一个响应者对象。当你的App接收到一个事件时,UIKit自动引导事件到最合适的响应者对象,也叫做第一响应者。

App使用响应者对象接收和处理事件,响应者对象是任何UIResponder的实例。UIResponder的子类包括UIView,UIViewController,UIApplication等。响应者接收到原始事件数据,必须处理事件或者转发到另一个响应者对象。当你的App接收到一个事件时,UIKit自动引导事件到最合适的响应者对象,也叫做第一响应者。

简单来说就是 :一级一级的找到响应的视图,如果没有就传给UIWindow实例和UIApplication实例,要是他们也处理不了,就丢弃这次事件...

int main(int argc, charchar * argv[]) {  
    @autoreleasepool {  
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));  
    }  
}  

不能处理的事件被传递到响应链中,这是App响应者对象动态配置的。在App中没有单一的响应链,UIKit定义了默认的规则关于对象如何被传递在一个响应者到另一个响应者,但是你可以重写响应者对象中适当的属性来改变这些规则。

不能处理的事件被传递到响应链中,这是App响应者对象动态配置的。在App中没有单一的响应链,UIKit定义了默认的规则关于对象如何被传递在一个响应者到另一个响应者,但是你可以重写响应者对象中适当的属性来改变这些规则。

 

根据main函数的参数加载UIApplication->AppDelegate->UIWindow->UIViewController->superView->subViews关系为:UIApplication.keyWindow.rootViewController.view.subView事件传递机制:
1.当iOS程序中发生触摸事件后,系统会将事件加入到UIApplication管理的一个任务队列中
2.UIApplication将处于任务队列最前端的事件向下分发。即UIWindow。
3.UIWindow将事件向下分发,即UIView。
4.UIView首先看自己是否能处理事件,触摸点是否在自己身上。如果能,那么继续寻找子视图。
5.遍历子控件,重复以上两步。
6.如果没有找到,那么自己就是事件处理者。
7.如果自己不能处理,那么不做任何处理。
其中 UIView不接受事件处理的情况主要有以下三种1)alpha <0.012)userInteractionEnabled = NO3.hidden = YES

下图是官方给出的一个默认响应链:新葡京8455 1Default Responder Chain

下图是官方给出的一个默认响应链:

对于IOS设备用户来说,他们操作设备的方式主要有三种:触摸屏幕、晃动设备、通过遥控设施控制设备。对应的事件类型有以下三种:

以下来自网络:

App中包含一个UILable,UITextField,UIButton,以及2个backgroundView,如果UITextField不能响应事件,UIKit发送事件到UITextField的父视图对象,随后是UIWindow的根视图。从根视图,响应者链在事件传递到UIWindow之前,先转移到所拥有的UIViewController。如果UIWindow不能处理事件,UIKit传递事件到UIApplication对象,也可能到app delegate如果对象是UIResponder的实例并且不是响应链的一部分。

新葡京8455 2

 

响应者链条概念: iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。
UIResponder 是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的 UIApplication、 UIViewController、 UIWindow 和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象。
UIWindow实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。
hitTest:withEvent:方法的处理流程如下: 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内; 若返回NO,则hitTest:withEvent:返回nil; 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕; 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束; 如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。

事件的每个类型,UIKit指定一个第一响应者,然后最先发送事件到这个对象。第一响应者基于事件的类型而变化。

Default Responder Chain

1、触屏事件(Touch Event)

一次完整的触摸事件的传递响应的过程UIApplication --> UIWindow --> 递归找到最适合处理事件的控件控件调用touches方法 --> 判断是否实现touches方法 --> 没有实现默认会将事件传递给上一个响应者 --> 找到上一个响应者
PS:如果直到UIApplication都不响应,那么这个事件就被废弃了。

  • Touch event第一响应者是触摸事件产生的view
  • Press event第一响应者是焦点响应者。
  • Shake-motion events,Remote-control events,Editing menu messages第一响应者是你或者UIKit指定的对象。

App中包含一个UILable,UITextField,UIButton,以及2个backgroundView,如果UITextField不能响应事件,UIKit发送事件到UITextField的父视图(UIView)对象,随后是UIWindow的根视图(UIView)。从根视图,响应者链在事件传递到UIWindow之前,先转移到所拥有的UIViewController。如果UIWindow不能处理事件,UIKit传递事件到UIApplication对象,也可能到app delegate如果对象是UIResponder的实例并且不是响应链的一部分。

 

1.响应者链条:由很多响应者链接在一起组合起来的一个链条响应者:继承自UIResponder的对象称之为响应者对象
2.上一个响应者(默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理) 如何判断当前响应者的上一个响应者是谁?1>判断当前是否是控制器的View,如果是,上一个响应者就是控制器
2>如果当前不是控制器的View,上一个响应者就是父控件

注意:运动事件相关的加速度计、陀螺仪、磁强计都不属于响应者链。而是由CoreMotion传递事件给你指定的对象。Core Motion

控件直接与它相关的target对象使用action消息通信。当用户与控件交互时,控件调用target对象的action方法,换句话说,控件发送action消息到目标对象。Action消息不是事件,但是它仍然可以利用响应链。当控件的target对象为nil,UIKit从target对象和响应链走,直到找到一个对象实现了合适的action方法。

如果视图有添加手势识别器,手势识别器接收touch和press事件在视图接收事件之前。如果所有的视图的手势识别器都不能识别它们的手势,这些事件会传递到视图处理。如果视图不能处理它们,UIKit传递事件到响应链。

UIKit使用基于视图的hit-testing来确定Touch事件在哪里产生。UIKit将Touch位置与视图层级中的视图对象的边界进行了比较。UIView的hitTest:withEvent:方法在视图层级中执行,寻找最深的包含指定Touch的子视图,这个视图将成为Touch事件的第一响应者。

确定事件的第一响应者

2、运动事件(Motion Event)

3.响应者链条有什么用?
可以让一个触摸事件发声的时候让多个响应者同时响应该事件在子类的实现文件里的touchesBegan:方法里加上如下代码即可[super touchesBegan:touches withEvent:event]

注意:如果Touch位置超过视图边界,hitTest:withEvent方法将忽略这个视图和它的所有子视图。结果就是,当视图的ciipsToBounds属性为NO,子视图超过视图边界也不会返回,即使它们包含发生的Touch。

UIKit不变的分配每一个Touch给包含它的视图。UIKit创建UITouch对象当touch第一次产生时,释放这个UITouch对象在touch结束时。当touch位置或者其他参数改变时,UIKit更新UITouch对象新的信息。只有包含它的视图这个属性不会改变。甚至这个touch位置移动刀初始视图的外面,这个属性也不会改变。

事件的每个类型,UIKit指定一个第一响应者,然后最先发送事件到这个对象。第一响应者基于事件的类型而变化。

 

总结:

hitTest:withEvent

这个方法返回最远的子视图在视图层级中,这个子视图是能接收包含指定点的。这个方法遍历视图层级让每个子视图调用poiotInside:withEvent:方法确定哪个子视图应该接收这个touch事件。如果poiotInside:withEvent: 返回YES,那么子视图的层次是类似遍历,直到找到最前面的视图包含指定点的。如果一个视图不包含该点,那么其分支视图可以被忽略。很少需要自己调用这个方法。但是可以重写它去隐藏touch事件在子视图中。

这个方法忽略以下情况:

  • 视图是隐藏的 hidden = YES
  • 用户交互关闭的 userInteractionEnabled = NO
  • 透明度小于0.01的 alpha < 0.01

这个方法在确定命中的时候,不考虑视图的内容。因此,即使指定的点位于该视图内容的透明范围,仍然可以返回视图。

点在接收者的范围之外不会被命中,即使它们实际上处于接收者的子视图之内。如果当前视图的cilpsToBounds属性被设置为NO,影响了子视图超过当前视图会产生这种情况。

你可以改变响应链通过重写你的响应对象的nextResponder属性。当你这样做了之后,下一个响应者就是你设置的。许多UIKit的类已经重写了这个属性然后返回了指定的对象。

  • UIView 如果视图是ViewController的根视图,下一个响应者为ViewController,否者是视图的父视图。
  • UIViewController 如果视图控制器是window的根视图下一个响应者为window对象。如果视图控制器是由另一个视图控制器推出来,那么下一个响应者为正在推出的视图控制器。-UIWindow 下一个响应者为UIApplication对象。
  • UIApplication 下一个响应者为app delegate,但是代理应该是UIResponder的一个实例 而不是 UIView,UIViewController或者app对象本身。
只看理论肯定是很迷茫的,下面我通过简单的一些示例代码演示部分内容。

在iOS中能够响应事件的都是UIResponder的子类对象。 UIResponder里有4个点击回调的方法。

- touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;- touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;- touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;- touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

参数里可以看到有一个UITouch和一个UIEvent对象,分别代表点击对象和事件对象。

为了便于测试我先添加了一个UIView类别。

#import "UIView Responder.h"static inline void swizzling_exchangeMethod(Class class ,SEL originalSelector, SEL swizzledSelector) { Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if  { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); }else { method_exchangeImplementations(originalMethod, swizzledMethod); }}@implementation UIView (Responder)  load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ swizzling_exchangeMethod([UIView class], @selector(touchesBegan:withEvent:), @selector(ds_touchesBegan:withEvent:)); swizzling_exchangeMethod([UIView class], @selector(touchesMoved:withEvent:), @selector(ds_touchesMoved:withEvent:)); swizzling_exchangeMethod([UIView class], @selector(touchesEnded:withEvent:), @selector(ds_touchesEnded:withEvent:)); });}#pragma mark - - ds_touchesBegan: touches withEvent: (UIEvent *)event{ NSLog(@"%@ touch begin", self.class); UIResponder *next = [self nextResponder]; while  { NSLog(@"%@",next.class); next = [next nextResponder]; }}- ds_touchesMoved: touches withEvent: (UIEvent *)event{ NSLog(@"%@ touch move", self.class);}- ds_touchesEnded: touches withEvent: (UIEvent *)event{ NSLog(@"%@ touch end", self.class);}

接着创建了4个继承于UIView的子View,AView的子视图为 BView、DView。BView的子视图为CView。

新葡京8455 3视图层级

首先是模拟官方的例子,我们点击CView,控制台输出如下:

新葡京8455 4寻找响应者.png

因为CView并不能响应这个事件,所以会一直往上寻找,和官方给的例子完全符合。如果view上有手势呢?给AView添加一个单击手势。

 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(aviewAction)]; [aview addGestureRecognizer:tap];- aviewAction { NSLog;}

单击之后控制台显示:

新葡京8455 5识别到手势.png

长按后

新葡京8455 6长按后没识别到手势事件交给视图.png

可以发现,无论有没有手势都会调用begin方法,如果识别到手势,UIView自己的end方法不调用了,会执行单击事件。如果没有识别到手势,则会调用end方法,接着交给UIView自己处理。至于响应链的输出在前面是因为我写在了begin方法里,在使用正常使用场景里,我们点击完松开手了才响应事件,也就是end之后才响应,有手势就执行手势方法 忽略了end,所以说手势接收事件在视图接收事件之前。

Touch event

3、远端控制事件(Remote-Control Event)

事件的传递和响应分两个链:

现在来看一下系统是怎么通过hit-test找到究竟是哪一个View产生的Touch,也就是包含Touch事件。

为了模拟系统的实现,在 load()方法里添加。然后写一下方法实现。

swizzling_exchangeMethod([UIView class], @selector(hitTest:withEvent:), @selector(ds_hitTest:withEvent:));swizzling_exchangeMethod([UIView class], @selector(pointInside:withEvent:), @selector(ds_pointInside:withEvent:));

//模拟一下,系统真正的实现肯定不是这样的,毕竟事件我都没用上。。- ds_hitTest: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]; //递归调用hitTest:withEvent继续判断 UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { //在这里打印self.class可以看到递归返回的顺序。 return hitTestView; } } //这里就是该视图没有子视图了 点在该视图中,所以直接返回本身,上面的hitTestView就是这个。 NSLog(@"命中的view:%@",self.class); return self; } //不在这个视图直接返回nil return nil;}- ds_pointInside:point withEvent:(nullable UIEvent *)event { BOOL success = CGRectContainsPoint(self.bounds, point); if  { NSLog(@"点在%@里",self.class); }else { NSLog(@"点不在%@里",self.class); } return success;}

我点击了CView,控制台输出如下:

新葡京8455 7Touch事件的产生以及响应者链.png

从这里可以看出会从UIWindow一层层的开始往子视图查找,直到找到一个视图,touch点还在这个视图里,但是该视图没有子视图,这个就是最深层的。在这里我也不明白为什么会调用2次,没找到相关资料。但是看名字应该是导航栏上的那些,最后命中的是UIStatusBarWindow,我感觉应该就是UIWindow后面的一层吧,但是UIWindow又不是加在它上面的,否则不会命中它。在这里,就是响应链了。命中CView后,立即调用了begin方法。

至于其他情况和其他视图的点击,我这里就不贴出来了。把上面代码拿去测试一下就行了。

不规则图形的点击事件,或者扩大缩小点击范围,还有像Tarbar中间那个凸起的按钮我感觉用这个也可以实现 ,只要重写pointInside:withEvent:方法就行了。

觉得对你有帮助点个赞吧。有什么错误欢迎指出,谢谢。

第一响应者是触摸事件产生的view

 

传递链:由系统向离用户最近的view传递。UIKit –> active app’s event queue –> window –> root view –>……–>lowest view
响应链:由离用户最近的view向系统传递。initial view –> super view –> …..–> view controller –> window –> Application

Press event

响应者链条概念: iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队 列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用 hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为 hit-test view。

第一响应者是焦点响应者。

 

Shake-motion events,Remote-control events,Editing menu messages

响应者对象(Responder Object) 指的是 有响应和处理事件能力的对象。 响应者链就是由一系列的响应者对象 构成的一个层次结构。

第一响应者是你或者UIKit指定的对象。

 

注意:运动事件相关的加速度计、陀螺仪、磁强计都不属于响应者链。而是由CoreMotion传递事件给你指定的对象。Core Motion

UIResponder 是所有响应对象的基类,在UIResponder类中定义了处理上述各种事件的接口。我们熟悉的 UIApplication、 UIViewController、 UIWindow 和所有继承自UIView的UIKit类都直接或间接的继承自UIResponder,所以它们 的实例都是可以构成响应者链的响应者对象。

控件直接与它相关的target对象使用action消息通信。当用户与控件交互时,控件调用target对象的action方法,换句话说,控件发送action消息到目标对象。Action消息不是事件,但是它仍然可以利用响应链。当控件的target对象为nil,UIKit从target对象和响应链走,直到找到一个对象实现了合适的action方法。

 

如果视图有添加手势识别器,手势识别器接收touch和press事件在视图接收事件之前。如果所有的视图的手势识别器都不能识别它们的手势,这些事件会传递到视图处理。如果视图不能处理它们,UIKit传递事件到响应链。

UIWindow实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用 pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果 pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。

确定哪个响应者包含Touch事件

 

UIKit使用基于视图的hit-testing来确定Touch事件在哪里产生。UIKit将Touch位置与视图层级中的视图对象的边界进行了比较。UIView的hitTest:withEvent:方法在视图层级中执行,寻找最深的包含指定Touch的子视图,这个视图将成为Touch事件的第一响应者。

hitTest:withEvent:方法的处理流程如下: 
首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内; 
若返回NO,则hitTest:withEvent:返回nil; 
若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕; 
若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束; 
如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。 

注意:如果Touch位置超过视图边界,hitTest:withEvent方法将忽略这个视图和它的所有子视图。结果就是,当视图的ciipsToBounds属性为NO,子视图超过视图边界也不会返回,即使它们包含发生的Touch。

 

UIKit不变的分配每一个Touch给包含它的视图。UIKit创建UITouch对象当touch第一次产生时,释放这个UITouch对象在touch结束时。当touch位置或者其他参数改变时,UIKit更新UITouch对象新的信息。只有包含它的视图这个属性不会改变。甚至这个touch位置移动刀初始视图的外面,这个属性也不会改变。

新葡京8455 8

hitTest:withEvent

 

这个方法返回最远的子视图在视图层级中,这个子视图是能接收包含指定点的(包括它本身)。

假如用户点击了View E,下面结合图二介绍hit-test view的流程:

这个方法遍历视图层级让每个子视图调用poiotInside:withEvent:方法确定哪个子视图应该接收这个touch事件。如果poiotInside:withEvent: 返回YES,那么子视图的层次是类似遍历,直到找到最前面的视图包含指定点的。如果一个视图不包含该点,那么其分支视图可以被忽略。很少需要自己调用这个方法。但是可以重写它去隐藏touch事件在子视图中。

 

这个方法忽略以下情况:

1、A是UIWindow的根视图,因此,UIWindwo对象会首相对A进行hit-test;

视图是隐藏的 hidden = YES

 

用户交互关闭的 userInteractionEnabled = NO

2、显然用户点击的范围是在A的范围内,因此, pointInside:withEvent:返回了YES,这时会继续检查A的子视图;

透明度小于0.01的 alpha < 0.01

 

这个方法在确定命中的时候,不考虑视图的内容。因此,即使指定的点位于该视图内容的透明范围,仍然可以返回视图。

3、这时候会有两个分支,B和C:

点在接收者的范围之外不会被命中,即使它们实际上处于接收者的子视图之内。如果当前视图的cilpsToBounds属性被设置为NO,影响了子视图超过当前视图会产生这种情况。

 

改变响应链

点击的范围不再B内,因此B分支的 pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;

你可以改变响应链通过重写你的响应对象的nextResponder属性。当你这样做了之后,下一个响应者就是你设置的。

 

许多UIKit的类已经重写了这个属性然后返回了指定的对象。

点击的范围在C内,即C的 pointInside:withEvent:返回YES;

UIView 如果视图是ViewController的根视图,下一个响应者为ViewController,否者是视图的父视图。

 

UIViewController 如果视图控制器是window的根视图下一个响应者为window对象。如果视图控制器是由另一个视图控制器推出来,那么下一个响应者为正在推出的视图控制器。

4、这时候有D和E两个分支:

-UIWindow 下一个响应者为UIApplication对象。

 

UIApplication 下一个响应者为app delegate,但是代理应该是UIResponder的一个实例 而不是 UIView,UIViewController或者app对象本身。

点击的范围不再D内,因此D 的 pointInside:withEvent:返回NO,对应的hitTest:withEvent:返回nil;

只看理论肯定是很迷茫的,下面我通过简单的一些示例代码演示部分内容。

 

在iOS中能够响应事件的都是UIResponder的子类对象。 UIResponder里有4个点击回调的方法。

点击的范围在E内,即E的 pointInside:withEvent:返回YES,由于E没有子视图(也可以理解成对E的子视图进行hit-test 时返回了nil),因此,E的 hitTest:withEvent:会将E返回,再往回回溯,就是C的 hitTest:withEvent:返回 E--->>A的hitTest:withEvent:返回E。 

- (void)touchesBegan:(NSSet *)touches withEvent:(nullableUIEvent*)event;- (void)touchesMoved:(NSSet *)touches withEvent:(nullableUIEvent*)event;- (void)touchesEnded:(NSSet *)touches withEvent:(nullableUIEvent*)event;- (void)touchesCancelled:(NSSet *)touches withEvent:(nullableUIEvent*)event;

 

参数里可以看到有一个UITouch和一个UIEvent对象,分别代表点击对象和事件对象。

至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑成功的找到了。

为了便于测试我先添加了一个UIView类别。

 

#import"UIView Responder.h"staticinlinevoidswizzling_exchangeMethod(Classclass,SEL originalSelector, SEL swizzledSelector) {    Method originalMethod = class_getInstanceMethod(class, originalSelector);    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);BOOLsuccess = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));if(success) {        class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));    }else{        method_exchangeImplementations(originalMethod, swizzledMethod);    }}@implementationUIView(Responder) (void)load {staticdispatch_once_tonceToken;dispatch_once(&onceToken, ^{        swizzling_exchangeMethod([UIViewclass],@selector(touchesBegan:withEvent:),@selector(ds_touchesBegan:withEvent:));        swizzling_exchangeMethod([UIViewclass],@selector(touchesMoved:withEvent:),@selector(ds_touchesMoved:withEvent:));        swizzling_exchangeMethod([UIViewclass],@selector(touchesEnded:withEvent:),@selector(ds_touchesEnded:withEvent:));    });}#pragma mark -- (void)ds_touchesBegan: (NSSet*)touches withEvent: (UIEvent*)event{NSLog(@"%@ touch begin",self.class);UIResponder*next = [selfnextResponder];while(next) {NSLog(@"%@",next.class);        next = [next nextResponder];    }}- (void)ds_touchesMoved: (NSSet*)touches withEvent: (UIEvent*)event{NSLog(@"%@ touch move",self.class);}- (void)ds_新葡京8455,touchesEnded: (NSSet*)touches withEvent: (UIEvent*)event{NSLog(@"%@ touch end",self.class);}

不难看出,这个处理流程有点类似二分搜索的思想,这样能以最快的速度,最精确地定位出能响应触摸事件的UIView。

接着创建了4个继承于UIView的子View,AView的子视图为 BView、DView。BView的子视图为CView。

 

新葡京8455 9

***上面找到了事件的第一响应者,接下来就该沿着寻找第一响应者的相反顺序来处理这个事件,如果UIWindow单例和UIApplication都无法处理这一事件,则该事件会被丢弃。***

视图层级

 

首先是模拟官方的例子,我们点击CView,控制台输出如下:

说明:

新葡京8455 10

 

寻找响应者.png

1、如果最终 hit-test没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow实例和UIApplication实例都不能处理该事件,则该事件会被丢弃;

因为CView并不能响应这个事件,所以会一直往上寻找,和官方给的例子完全符合。

 

如果view上有手势呢?给AView添加一个单击手势。

2、hitTest:withEvent:方法将会忽略隐藏(hidden=YES)的视图,禁止用户操作 (userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。如果一个子视 图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的 pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写 pointInside:withEvent:方法来处理这种情况。

UITapGestureRecognizer*tap = [[UITapGestureRecognizeralloc] initWithTarget:selfaction:@selector(aviewAction)];    [aview addGestureRecognizer:tap];- (void)aviewAction {NSLog(@"单击");}

 

单击之后控制台显示:

3、我们可以重写 hitTest:withEvent:来达到某些特定的目的,下面的链接就是一个有趣的应用举例,当然实际应用中很少用到这些。

新葡京8455 11

 

识别到手势.png

 

长按后

 

新葡京8455 12

再深入详细一些:

长按后没识别到手势事件交给视图.png

 

可以发现,无论有没有手势都会调用begin方法,如果识别到手势,UIView自己的end方法不调用了,会执行单击事件。如果没有识别到手势,则会调用end方法,接着交给UIView自己处理。至于响应链的输出在前面是因为我写在了begin方法里,在使用正常使用场景里,我们点击完松开手了才响应事件,也就是end之后才响应,有手势就执行手势方法 忽略了end,所以说手势接收事件在视图接收事件之前。

iOS的事件大致分为三种:触摸事件、加速计事件、远程控制事件

现在来看一下系统是怎么通过hit-test找到究竟是哪一个View产生的Touch,也就是包含Touch事件。

  首先要理解以下几个概念:

为了模拟系统的实现,在 (void)load()方法里添加。然后写一下方法实现。

  一、响应者对象:

swizzling_exchangeMethod([UIView class],@selector(hitTest:withEvent:),@selector(ds_hitTest:withEvent:));swizzling_exchangeMethod([UIView class],@selector(pointInside:withEvent:),@selector(ds_pointInside:withEvent:));

    在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”。

//模拟一下,系统真正的实现肯定不是这样的,毕竟事件我都没用上。。- (UIView*)ds_hitTest:(CGPoint)point withEvent:(UIEvent*)event {if(!self.isUserInteractionEnabled ||self.isHidden ||self.alpha <=0.01)returnnil;//判断点在不在这个视图里if([selfpointInside:point withEvent:event]) {//在这个视图 遍历该视图的子视图for(UIView*subviewin[self.subviews reverseObjectEnumerator]) {//转换坐标到子视图CGPointconvertedPoint = [subview convertPoint:point fromView:self];//递归调用hitTest:withEvent继续判断UIView*hitTestView = [subview hitTest:convertedPoint withEvent:event];if(hitTestView) {//在这里打印self.class可以看到递归返回的顺序。returnhitTestView;            }        }//这里就是该视图没有子视图了 点在该视图中,所以直接返回本身,上面的hitTestView就是这个。NSLog(@"命中的view:%@",self.class);returnself;    }//不在这个视图直接返回nilreturnnil;}- (BOOL)ds_pointInside:(CGPoint)point withEvent:(nullableUIEvent*)event {BOOLsuccess =CGRectContainsPoint(self.bounds, point);if(success) {NSLog(@"点在%@里",self.class);    }else{NSLog(@"点不在%@里",self.class);    }returnsuccess;}

    UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件。

我点击了CView,控制台输出如下:

  二、UIResponder处理事件的主要方法

新葡京8455 13

    触摸事件    

Touch事件的产生以及响应者链.png

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

从(1)这里可以看出会从UIWindow一层层的开始往子视图查找,直到找到一个视图,touch点还在这个视图里,但是该视图没有子视图,这个就是最深层的。

    加速计事件

在(2)这里我也不明白为什么会调用2次,没找到相关资料。但是看名字应该是导航栏上的那些,最后命中的是UIStatusBarWindow,我感觉应该就是UIWindow后面的一层吧,但是UIWindow又不是加在它上面的,否则不会命中它。

- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

在这里,(3)就是响应链了。命中CView后,立即调用了begin方法。

    远程控制事件

至于其他情况和其他视图的点击,我这里就不贴出来了。把上面代码拿去测试一下就行了。

- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

实际使用

  三、UITouch

不规则图形的点击事件,或者扩大缩小点击范围,

    用户同时触摸的手指,每一根就代表一个UITouch对象,它保存着跟手指相关的信息,比如触摸的位置、时间、阶段。

还有像Tarbar中间那个凸起的按钮我感觉用这个也可以实现(这个我自己没试过) ,只要重写pointInside:withEvent:方法就行了。

    当手指移动(move)时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。

作者:HelloAda

    当手指离开(end)屏幕时,系统会销毁相应的UITouch对象.

链接:

 

來源:简书

  四、UIEvent

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    触摸事件发生,必须会产生一个UIEvent对象,称为事件对象,记录事件产生的时刻和类型。

 

  五、首先处理事件的响应者

    发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中。

    UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)

    主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步

    找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理

    touchesBegan…

    touchesMoved…

    touchedEnded…

    

    下面是PPT示例,说的很明白。

  新葡京8455 14

  UIView不接收触摸事件的三种情况

  1.alpha= 0.0 ~ 0.01

  2.hidden属性=YES

  3.userInteractionEnabled=YES

  注意:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的

 

  六、响应者链条

    合适的控件调用touches方法后,会默认地将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理。

  寻找上一个响应者的过程,看起来是寻找首要合适控件的逆过程。

  当前响应者的touches方法中如果调用[super touches],就会对用上一个响应者touches方法,同时可以将UITouch对象和UIEvent对象向上传递。

  这样就能够将一次触摸事件的事件对象和触摸对象的信息传递给多个响应者。

 

  问题:上一个响应者(nextResponder)是谁?

  判断步骤:
  1>如果当前的view是控制器的view,控制器就是上一个响应者
  2>如果当前的view不是控制器的view,那么父控件就是上一个响应者
  3>如果当前响应者是控制器,那么上一个响应者是UIWindow;如果UIWindow也不处理,就再往上传给UIApplication
  4>如果UIApplication也不处理,那么这条消息就被会废弃

  新葡京8455 15

  七、触摸事件处理过程

  1.用户触摸后,系统先将事件对象(event)由上往下传递(父控件传给子控件),找到最合适的控件来处理事件(递归查找当前控件的最适合子控件)

  2.调用合适控件的touches相关方法

  3.如果调用了super的touches相关方法,就会将事件顺着响应者链条往上传递,传给上一个响应者

  4.接着就会调用上一个响应者的touches方法

  5.只要当前响应者的touches方法中调用了super的touches方法,还会继续往上递归调用,直到不再调用super的touches方法

本文由新葡京8455发布,转载请注明来源

关键词: