触摸事件
触摸事件处理的完整过程
- 发生触摸事件后,系统会将该事件加入到一个由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]; }