事件处理原理(IOS篇) by sixleaves
前言
了解IOS事件处理的本质关键要先掌握几个概念。首先是事件的派发(Event Delivery)的过程, 一个是响应者链条如何构成。
事件的派发:
Q1: 你有没有想过,如果你一个屏幕中有多个的View。当你点击某个view的时候, 这个点击事件是如何传递到这个View身上的?
S1: 正是因为当我们点击屏幕上某个点的时候, IOS会检查到手指触摸操作(Touch),并生产一个UITouch对象,将其打包成一个UIEvent对象。然后将其放入当前活动的Application的事件对列, UIApplication会从事件对列中按照对列的顺序,取出触摸事件传递给UIWindow处理,UIWindow对象会使用hitTest:withEvent:方法来寻找此次的触摸操作初始点所在的最深层次的视图(View). **即调用hitTest:withEvent会返回该触摸点所在的最深层次的视图。 **
Q2:hitTest:withEvent如何实现找到最深层次的视图,也就是目的视图。
S2: 这就要说到深度优先搜索算法,hitTest:withEvent正是基于深度优先搜索的方式来找到最深层次的视图对象。所以我来介绍以下深度优先算法的思想, 要理解该思想, 你首先要有树结构这一概念(参见数据结构中的树结构)。该思想是从根节点开始遍历树,而遍历的顺序是采用把下一个子节点当做当前根节点继续遍历。 所以其是先遍历到最树的最深的一层再层层回朔到根节点,接着在把另外一个子节点当做当前根节点继续遍历。正是基于这种思想, 所以我们可以很方便的采用递归来实现。 如果还是不理解,有两种办法帮助你,一种是去找深度优先的动态图,一看就懂了我说的。另外一种方法是去复习数据结构与算法。
Q3:既然hitTest:withEvent利用了深度优先的思想来做,并采用递归的做法来做。那么递归结束的条件是啥?也就是说什么条件下事件不会向下派发?
S3:根据官方文档给出的条件是(hidden == YES || userInteractionEnabled == YES || alpha < 0.01 || subViews.bounds > subViews.superView.bounds)
何为响应者链条
Q4: 也许你经常听别人在说响应者链条,但是还是云里雾里。这边我就给你解释下
S4: 首先先明确何为响应者? ===> 在ios开发中继承自UIResponder的类或子类就是响应者,顾明思意,响应者是用来相应事件的(触摸事件、运动事件、远程遥控事件)。所以所谓的响应者链条就是一系列响应者构成的层次结构。
Q5: 那么响应者链条是如何表示这种层次结构的呢?
S5: 响应者链条是通过nextResponder方法的返回值来组成这种层次结构的 ,苹果有一段官方解释如下:
The UIResponder class does not store or set the next responder automatically, instead returning nil by default. Subclasses must override this method to set the next responder. UIView implements this method by returning the UIViewController object that manages it (if it has one) or its superview (if it doesn’t); UIViewController implements the method by returning its view’s superview; UIWindow returns the application object, and UIApplication returns nil.
也就是说,响应者对象是不会自动设置和存储下一个响应者,默认情况下是直接返回nil。而继承自UIResponder的子类必须重写这个方法来设置下一个响应者,并且需要遵循如下规范
1. 如果子类是UIView,那么其getter方法的nextResponder必须返回其UIViewController对象。
如果不存在控制器,则返回其父视图对象。
2. 如果子类是UIViewController对象, 那么重写的nextResponder方法必须返回其view视图的父视图对象。
3. 如果子类是UIWindow对象,那么重写的nextResponder方法返回的是application对象
4. 如果子类是UIApplication对象,那么重写的nextResponder方法,返回nil。
通过上述规范,结合下图,你应该能很容易理解所谓的响应者链条如何构成:
Q6: 疑问,假设我有一个自定义的SWPButton,而且给其设置了连线action(也就是点击按钮后回调的函数)。我们知道当我们点击按钮的时候系统会捕捉到这个事件,并将其派发下来。那么我们如何做到让按钮不响应这个action而是只响应这个事件。
- S6: 本质上系统默认实现的touchesBegan:withEvent:方法是做了两件事件, 或者说有继承子UIResponder的系统定义的类,都默认实现了以下两件事情
- 第一件事情是: 调用[super touchesBegan:withEvent]让父类有机会来处理该事件。 同时传递了事件,让父类能根据事件处理相应的action回调,比如父类会在这里面处理action(也就是点击按钮时候的回调)
- 第二件事情是: 抛给上一个响应者,让上一个响应者也能处理该事件。同时传递了该事件。
这两个是不一样的,前者是针对父类,后者是针对响应者链条。二者缺一不可。
再者,有的人会说只要实现了[super toucesBegan:withEveent]就会将事件抛开上一个响应者,这是不严谨的说法。因为这是因为系统已经为所有继承自UIResponder的子类,做了上面那两件事件。
比如 ,当你自定义一个SWPButton该类继承自UIButton,那么当你重写touchesBegan方法的时候,在你们调用[super touchesBegan:withEvent]这个方法时候,系统自定义的UIButton类已经具备这个功能,所以
其能完成向上抛。而当我们定义一个纯洁的SWPButton直接继承自UIResponder, 那么我们必须自己实现向上抛这个事情,而不是只调用[super touchesBegan:withEvent]。
华丽的分割线,以下是度对Q6的解答 因为我们继承自UIButton,所以要截断该action的回调,我们只需重写touchesBegan:withEvent方法。并且不做第一件事件,那么也就不会将事件传递给Button对象,button也就无法响应相应的action。
#import "SWPButton.h"
@implementation SWPButton
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesBegan:touches withEvent:event];
}
@end
所以此时如果你点击按钮, 其会先调用这个自定义按钮的touchesBegan, 因为点击事件传递到了button身上,所以会调用touchesBegan来响应该事件,但是该事件不在交给父类处理,所以不会调用action。只会继续将其抛给上一个响应者。
放大招(根据View来找到其所在控制器):
@implementation UIView (ParentController)
-(UIViewController*)parentController{
UIResponder *responder = [self nextResponder];
while (responder) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController*)responder;
}
responder = [responder nextResponder];
}
return nil;
}
@end
思路很简单,就是利用响应者链条来寻找UIViewController.
那么这个UIView的上一个响应者只有两种情况,一种是依然是一个UIView对象或其子对象,也就是说它是这个UIView的子View。
一种是这个 UIView的控制器。所以我们才需要循环判断,然后不断找(循环),直到第一次找到的控制器,就是这个UIView所在的控制器。
如果找不到,就返回nil。
是不是更加理解你事件派发的过程,所谓的事件派发过程,其实就是寻找最合适的视图的时候,事件随着这个寻找过程,不断传递。 为什么要传递UIEvent呢?因为通过它给以获得Touch对象,而通过Touch对象我们可以获得初始触摸点。也就是说hitTest:withEvent主要实现的功能是,传递事件,找到最合适的视图。