在 iOS 中,视图的层级一般都是 父视图 -> 添加各种子视图。这时候某个视图(子视图)上有个按钮,需要我们交互。但是有时候我们会发现无论如何都没有反应。这时候可能就是我们对 iOS 的事件传递响应还有些迷茫。
响应者对象(UIResponder)
在 iOS 中,只要是继承 UIResponder 的对象都可以接收并处理事件。在 iOS 中提供了一些方法来处理触摸事件。
- - (void) touchesBegan: (NSSet * ) touches withEvent: (nullable UIEvent * ) event; // 开始触摸View时会调用一次
- - (void) touchesMoved: (NSSet * ) touches withEvent: (nullable UIEvent * ) event; // 随着手指一动会多次调用
- - (void) touchesEnded: (NSSet * ) touches withEvent: (nullable UIEvent * ) event; // 手指离开的时候会调用
- - (void) touchesCancelled: (NSSet * ) touches withEvent: (nullable UIEvent * ) event; // 触摸结束前,电话打进来,会自动调用这个方法
事件的产生
当发生一个触摸事件后,系统会将触摸事件添加到 UIApplication 管理的事件队列中(先进先出) -> UIApplication 从事件队列中拿出最前的事件将之分发出去,通常是首先发送事件给应用程序的主窗口 -> 主窗口会找到一个最合适的视图来处理触摸事件 -> 找到合适的视图控件后,就会调用控件的上述方法中的一个或者多个来处理具体的事件处理。
事件的传递
主窗口先判断能不能接收这个触摸事件,如若不能,就直接 return;
主窗口可以接收,传递给子视图,继续判断,继续传递,循环直到没有能够符合响应的子控件,那么这时候的就会认为由自己来处理这个事件最合适。
也有不能响应的情况:
1. 不允许交互
2. 控件隐藏
3. 透明度过低(<0.01)
如何寻找最适合的控件来处理事件
UIView 及其子类有两个非常重要的方法
- - (nullable UIView * ) hitTest: (CGPoint) point withEvent: (nullable UIEvent * ) event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- - (BOOL) pointInside: (CGPoint) point withEvent: (nullable UIEvent * ) event; // default returns YES if point is in bounds
当只要有事件传递给这个控件,这个控件就会调用
- hitTest: withEvent:
其作用是寻找并返回最合适的 View,不管这个控件能不能处理事件,也不管触摸点是不是在这个空间上,都会先接收事件,然后调用方法。
所以这里我们就有了可操作空间 , 因为不管点击事件发生在哪里,最终能够处理事件的 View 都是这个方法返回的 View。通过重写这个方法我们可以拦截整个事件的传递过程,同时可以指定处理事件的 View。(如果这个方法返回的是 nil,那么调用该方法的控件本身以及其子控件均不能处理事件,只能由其父视图来处理事件)
所以事件的传递顺序 :产生触摸事件 -> UIApplication 事件队列 -> [UIWindow hitTest:withEvent:] -> 返回更合适的 View -> [子控件 hitTest:withEvent:] -> 返回最合适的 View ...
所以这里我们可以得到的结论就是:不管子控件是不是最合适的 View,都会调用 hitTest 方法,如果不是最合适的 View,会返回 nil,同时认定其父视图是最合适的 View。
小技巧:在父控件中返回最合适的子控件。因为如果在自己返回自己,有可能两个视图 B,C 同时加载 A 上,当设置 B 为最合适的 View,这时候如果我们在 B 中返回自己,可能我们点击到 C 这时候 B 还没来及返回系统就已经定位到了 C 。
寻找最合适的 View 底层剖析
- // 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
- // 作用:寻找并返回最合适的view
- // UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
- // point:当前手指触摸的点
- // point:是方法调用者坐标系上的点
- - (UIView * ) hitTest: (CGPoint) point withEvent: (UIEvent * ) event {
- // 1.判断下窗口能否接收事件
- if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
- // 2.判断下点在不在窗口上
- // 不在窗口上
- if ([self pointInside: point withEvent: event] == NO) return nil;
- // 3.从后往前遍历子控件数组
- int count = (int) self.subviews.count;
- for (int i = count - 1; i >= 0; i--) {
- // 获取子控件
- UIView * childView = self.subviews[i];
- // 坐标系的转换,把窗口上的点转换为子控件上的点
- // 把自己控件上的点转换成子控件上的点
- CGPoint childP = [self convertPoint: point toView: childView];
- UIView * fitView = [childView hitTest: childP withEvent: event];
- if (fitView) {
- // 如果能找到最合适的view
- return fitView;
- }
- }
- // 4.没有找到更合适的view,也就是没有比自己更合适的view
- return self;
- }
通过重写 View 的 hitTest 方法,即可找到最合适的 View
另一个比较重要的方法
- pointInside: withEvent:
方法是用来判断我们触摸事件的点位置是否在当前 View 上,如果返回 NO 说明是不在当前 View 坐标系上,同时自然是不能够处理事件的。
事件的响应
传递方式是 从下往上 的传递方式。
事件处理流程
产生触摸事件 -> 事件添加到 UIApplication 队列中 -> 事件传递主窗口 -> 找到最合适的 View -> 最合适的 View 调用自己的 touch 方法来处理事件 -> touches 默认做法是把事件顺着响应链往上传递
- //只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
- - (void) touchesBegan: (NSSet * ) touches withEvent: (UIEvent * ) event {
- // 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
- [super touchesBegan: touches withEvent: event];
- // 注意不是调用父控件的touches方法,而是调用父类的touches方法
- // super是父类 superview是父控件
- }
当我们需要做到一个事件多个对象同时处理的话,我们就可以先处理自己的事件之后,调用 super 方法。
当我们要扩大按钮点击范围
比如我们有一个 20pt*20pt 的 按钮,我们可以在一个控件的中利用 hitTest 来实现。 例如一个 UIButton,自定义一个按钮,在其自定义类中重写方法。
- - (UIView * ) hitTest: (CGPoint) point withEvent: (UIEvent * ) event {
- // 1.判断下窗口能否接收事件
- if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
- // 扩大到按钮之外的都是点击范围
- CGRect touchRect = CGRectInset(self.bounds, -20, -20);
- if (CGRectContainsPoint(touchRect, point)) {
- for (UIView * subView in [self.subviews reverseObjectEnumerator]) {
- CGPoint convertedPoint = [subView convertPoint: point toView: self];
- UIView * hitTestView = [subView hitTest: convertedPoint withEvent: event];
- if (hitTestView) {
- return hitTestView;
- }
- }
- return self;
- }
- return nil;
- }
将事件传递给兄弟 View(A 与 B 是同一个父视图,但是 B 有部分遮挡住了 A ;点击遮挡部分需要 A 响应事件)这时候点击 A 是不会有任何响应的,除非 B 的 userInteractionEnable 为 NO , 但是我们用 hitTest 同样可以做到, 重写 B 的这个方法
- - (UIView * ) hitTest: (CGPoint) point withEvent: (UIEvent * ) event {
- UIView * hitTestView = [super hitTest: point withEvent: event];
- if (hitTestView == self) {
- hitTestView = nil;
- }
- return hitTestView;
- }
来源: http://www.cnblogs.com/wang-com/p/7242729.html