深入理解touch事件和响应者链
原文地址:http://nsomar.com/understanding-cocoa-and-cocoa-touch-responder-chain/
每一个cocoa应用都对应有一个事件队列来保存发生的事件,这个事件队列接受多种类型的事件源发出的事件。为了能够流畅的处理事件,每一个应用都在内部维护一个runloop来接受和转发事件。
当我们应用启动的时候会调用UIApplicationMain方法,这个方法将创建一个UIApplication的单例对象,这个对象将负责处理和转发事件队列中的事件。
应用的事件队列中一般存在如下三种事件类型:
- UIControl Aaction:如设定在按钮上的目标和处理方法对。
- 用户事件: 触摸,摇晃等事件。
- 系统事件: 内存,电量等系统事件.
每一个这样的事件在转发给目标处理对象之前都是需要application单例来管理的。
UIControl Actions:
UIControl actions是通过addTarget:action:forControlEvents:方法加载到UIControl控件上的事件,UIControl将会保存action/target对。
当用户出发了这个UIControl对象或者这个UIControl调用了sendActionsForControlEvents函数,那么将会去target指定的对象处调用action指定的方法(切记不是直接去调用,还是要先转发给application 单例管理的事件队列)
像这个例子:
UIButton button = [UIButton new]; [button addTarget:self action:@selector(buttonTapped) forControlEvents:UIControlEventTouchUpInside];
当用户点击了按钮,事件将发送给UIApplication(其实点击后UIControl内部就会调用sendActionsForControlEvents产生一个事件)存入队列,application读取队列中的action并使用UIApplication
sendAction:to:from:forEvent:函数,这个函数的最基本功能就是找到注册的target,然后想起发送注册的action事件。
如果我们将target指定为nil的时:
[button addTarget:nil action:@selector(buttonTapped) forControlEvents:UIControlEventTouchUpInside];
在这个场景下,sendAction:to:from:forEvent将会去寻找此时在响应者链中的第一响应者去处理。
如果第一响应者无法处理这个action(既本例中的buttonTapped方法),那么他将发送给响应者链的下一个响应者,系统将一直在响应者链中去寻找能够处理action的对象知道链表最后,那么这个action将会被抛弃。
使用这个特性我们可以很方便的去使用UIApplication的单例对象的 sendAction:to:from:forEvent方法然后将target设定为nil去给第一响应者发送ation了。
例如我们可以让第一响应者辞去第一响应者角色从而关闭键盘等:
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) to:nil from:nil forEvent:nil];
用户事件
如触摸和摇动设备等产生的用户事件都将发送到application的事件队列中,如果这个事件不是touch事件那么application会将此事件转发给第一响应者去处理,如果第一响应者无法处理,系统将沿着响应者链去寻找合适的事件处理对象。
对于touch事件来说处理流程上有点不一样,当系统感知到屏幕被touch了,他将这个touch事件发送给application,application将通过_touchesEvent内部方法收到touch事件
接着application会将使用sendEvent方法将这个事件转发给UIWindow对象,UIWindow对象就会发起hit-testing view流程去找寻处理该事件的view对象。
hit-testing的过程为是是使用hitTest:withEvent和
pointInside:withEvent:不断饿递归调用来找到被touch的view。
UIWindow 将会给这个被知道的view(即hit-test view)发送以下四种方法:
- (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;
对于接收到事件的view(hit-test view)会有如下几种处理选择:
- 如果当前View没有实现其中某个方法,那个这个方法将会在nextponder对象中去尝试调用。
- A view can implement any method from the above, do some processing, and then call super in order to let the next responder do some additional process.
- 这个类也可以实现上述所有方法,让后做出一些处理,接着直接调用父类的方法从而去让nextResponder 做额外的处理。
- 当然这个view可以实现所有的这些方法,全部自己处理完毕。不让next reponder参与进来。
当这个view不能处理touch事件的时候,那么他将此事件发送给响应者链,这个响应者链的路径是这样的:
- The first responder is the hit-tested view (the view under the touch)
- 第一响应者为hit-tested view(既被触摸的view)
- 下一个响应者为他的父view
- The chain continues up the view hierarchy until it reaches a view that is associated with a view controller
- 知道达到包含这些view的controller
- That view controller will be the next responder
- controller就作为下一个响应者了。
- If this view controller is a root controller, then the window will be the next responder
- 如果此时view controller是根controller,那么window对象将是下一响应者
- The application is the window's next responder
- application将是window对象的下一响应者
- The last responder in the chain is the App delegate
- 最后的响应者为app的代理
系统事件:
系统将会发送一些事件给Application的单例对象,这些和系统相关的事件被application单例接受到后将转送给app的代理。app 代理将会以此地去结束和处理他们。
第一响应者
UIResponder可以通过becomeFirstResponder方法来注册成为第一响应者,第一响应者将会第一个去处理用户事件,当然touch事件如前所述将不会让第一响应者去处理,他将通过hit-testing过程去找寻hit-tested view去处理。
另外,第一响应者也有责任去第一个处理那些没有指定target的UIControl action。