触摸事件
触摸事件处理的完整过程
- 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的
事件队列中 - UIApplication会从事件队列中取出
最前面的事件,并将事件分发下去以便处理,通常先发送事件给应用程序的主窗口 - 主窗口会在视图层次结构中找到
最合适处理事件的控件,这也是整个事件处理过程的第一步 - 找到
最合适处理事件的控件后,调用控件的touches...方法,touches方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理,直至事件处理完毕
事件传递
- 触摸事件的传递是
从父控件传递到子控件 - 如果父控件
不能接收触摸事件,那么子控件就不可能接收到触摸事件 不能接受触摸事件的四种情况不能与用户交互,即:userInteractionEnabled =NO隐藏,即:hidden = YES透明,即:alpha <= 0.01未启用,即:enabled = NO
提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的- 如何找到
最合适处理事件的控件:- 首先,判断自己
能否接收触摸事件,可以通过重写hitTest:withEvent:方法验证 - 其次,判断触摸点
是否在自己身上,对应方法pointInside:withEvent: - 从后往前(先遍历
最后添加的子控件)遍历子控件,重复前面的两个步骤 - 如果
没有找到符合条件的子控件,那么就自己处理
- 首先,判断自己
响应者链条
- 定义:是由
多个响应者对象连接起来的链条 - 响应者:能处理事件的对象
- 作用:能很清楚的看见每个响应者之间的联系,并且可以让
一个事件可以由多个对象处理 如何判断
上一个响应者- 如果当前这个view
是控制器的view,那么控制器就是上一个响应者 - 如果当前这个view
不是控制器的view,那么父控件就是上一个响应者
注意:继承于UIControl的控件有些特殊,下面用代码解释-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 按照正常情况会将事件传递给其父控件 // 但由于UIControl本身有处理事件的能力,所以事件【被拦截】,不会传递给其父控件 [super touchesBegan:touches withEvent:event]; // 如果真想把事件传递给父控件,可以这样做 // [self.nextResponder touchesBegan:touches withEvent:event]; }
- 如果当前这个view
- 响应者链条的传递过程
- 如果
是控制器的view,就传递给控制器; - 如果
不是控制器的view,则传递给它的父控件 - 如果视图层次结构的
最顶层视图也不能处理收到的事件或消息,则会将事件或消息传递给UIWindow对象进行处理 - 如果
UIWindow对象也不能处理,则将事件或消息传递给UIApplication对象 - 如果UIApplication也
不能处理该事件或消息,则将其丢弃
- 如果
验证事件传递和响应者链条的默认做法

模仿系统做法找到最合适的控件的做法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { NSLog(@"%@---%s",NSStringFromClass([self class]),__func__); // 系统默认做法 // return [super hitTest:point withEvent:event]; // 模仿系统做法找到最合适的控件的做法 return [self findTheRightView:point withEvent:event]; } /** * 模仿系统做法找到最合适的控件的做法 */ - (UIView *)findTheRightView:(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.从后往前遍历自己的子控件 NSInteger count = self.subviews.count; for (NSInteger i = count - 1; i >= 0; i--) { UIView *childView = self.subviews[i]; // 把当前控件上的坐标系转换成子控件上的坐标系 CGPoint childP = [self convertPoint:point toView:childView]; // 递归找到最合适处理事件的控件 UIView *rightView = [childView findTheRightView:childP withEvent:event]; if (rightView)return rightView; } // 循环结束,表示没有比自己更合适的处理事件的view return self; }hitTest:withEvent:方法妙用:可以重写这个方法来改变系统默认寻找最合适处理事件的view的处理逻辑(比如:可以控制一个按钮始终是最合适处理事件的控件,不管他上面是否有其他覆盖控件)pointInside:withEvent:方法妙用:可以重写这个方法来改变系统默认判断点是否在自己身上的逻辑pointInside:withEvent:方法与CGRectContainsPoint的区别- CGRectContainsPoint用于判断一个点是否在指定区域,左上角参考点(0,0)必须一致才有可比性
pointInside:withEvent:方法用于判断一个点(相对指定控件的左上角的(0,0)点确定的坐标)是否在当前控件内
/** * 控制返回最合适处理事件的控件 */ -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { // 将当前坐标点转化为相对按钮左上角(0,0)坐标点 CGPoint btnPoint = [self convertPoint:point toView:self.btn]; // CGRectContainsPoint用于判断一个点是否在指定区域,左上角参考点(0,0)必须一致才有可比性 // 由于btnPoint已经被转化为相对按钮的左上角的坐标点了,所以不能用此函数判断点是否在指定区域内 // BOOL isContain = CGRectContainsPoint(self.btn.frame, btnPoint); // NSLog(@"self.btn.frame-->%@",NSStringFromCGRect(self.btn.frame)); // NSLog(@"btnPoint-->%@",NSStringFromCGPoint(btnPoint)); // 只要点在按钮上就交由按钮处理对应的事件 if([self pointInside:btnPoint withEvent:event]) { return self.btn; } return [super hitTest:point withEvent:event]; }