在iOS中,视图的层级通常都是 父视图->添加各类子视图。这时候某个视图(子视图)上有个按钮,须要咱们交互。可是有时候咱们会发现不管如何都没有反应。这时候可能就是咱们对iOS的事件传递响应还有些迷茫。数组
响应者对象(UIResponder)app
在iOS中,只要是继承UIResponder的对象均可以接收并处理事件。在iOS中提供了一些方法来处理触摸事件。ide
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 开始触摸View时会调用一次 - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 随着手指一动会屡次调用 - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 手指离开的时候会调用 - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event; // 触摸结束前,电话打进来,会自动调用这个方法
事件的产生spa
当发生一个触摸事件后,系统会将触摸事件添加到UIApplication管理的事件队列中(先进先出) -> UIApplication 从事件队列中拿出最前的事件将之分发出去,一般是首先发送事件给应用程序的主窗口 -> 主窗口会找到一个最合适的视图来处理触摸事件 -> 找到合适的视图控件后,就会调用控件的上述方法中的一个或者多个来处理具体的事件处理。code
事件的传递对象
主窗口先判断能不能接收这个触摸事件,如若不能,就直接return;blog
主窗口能够接收,传递给子视图,继续判断,继续传递,循环直到没有可以符合响应的子控件,那么这时候的就会认为由本身来处理这个事件最合适。继承
也有不能响应的状况:队列
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; }