先来明确几个概念
- 响应者对象
可以进行事件处理的对象。用户进行了某个操作,系统会将该操作包装成一个Event事件对象交与响应者对象进行处理 - 事件传递
如果第一响应者对象没有处理该事件,系统就会将它转发到下一个响应者对象那里处理,这个寻找的过程就成为事件传递 - 事件队列
操作被包装成事件之后,会先放入当前活动的Application事件队列中,单例的UIApplication会从事件队列中依次取出事件并交给单例的UIWindow来处理,UIWindow对象首先会寻找此次操作的初始点所在的view - 响应链
进行事件传递的由一系列响应者对象组成的层次结构就称为响应者链 - 涉及到的方法 :hitTest:withEvent:
命中测试方法,该方法用于寻找第一响应者的过程中。返回值类型是UIView,表示当前事件触发的点在哪个view的范围内。hitTest方法的返回值,表示当前视图层级最低层的符合条件的视图。比如,Aview的子视图是Bview,点击了Bview,虽然这个点也在Aview范围内,但是hitTest会继续向它的子视图Bview进行查找,最终hitTest返回的是Bview
该方法的point参数是相对于接收者的坐标系的,其内部会首先调用pointInside:withEvent: 方法, - 涉及到的方法:pointInside:withEvent:
该方法的返回值类型为布尔型,判断参数中的点是否在当前所在view范围内。官方对该方法的注释是:default returns YES if point is in bounds - 第一响应者
如5所说,虽然Aview和Bview都可以是点击事件的响应者,但系统只会将该事件交与最底层的Bview进行处理,此时Bview就称为第一响应者
事件处理的过程
先将事件对象由父控件传递给子控件,找到最合适的控件来处理这个事件。 调用最合适控件的touches….方法 如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者 接着就会调用上一个响应者的touches….方法。
如果view是控制器的view,就传递给控制器;如不是,则将其传递给它的父视图 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理 如果window对象也不处理,则其将事件或消息传递给UIApplication对象 如果UIApplication也不能处理该事件或消息,则该事件会被丢弃,不能被处理
另外,在遍历子控件的时候,是从子控件数组中最后一个元素开始使用hitTest进行判断
两个方法的内部实现
大概的内部实现,只是为了方便理解
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *touchView = self;
// 事件发生点在当前view范围内,接着查询是否在子view范围内
if ([self pointInside:point withEvent:event]) {
for (UIView *subView in self.subviews) {
// 转换下坐标,
CGPoint subPoint = CGPointMake(point.x - subView.frame.origin.x,
point.y - subView.frame.origin.y);
// 调用自身方法检查子view
UIView *subTouchView = [subView hitTest:subPoint withEvent:event];
if (subTouchView) {
//找到touch事件对应的view,停止遍历
touchView = subTouchView;
break;
}
// 如果所有子视图的hitTest都返回nil,则该方法直接返回self
}
}else{
//不在该View范围内,直接返回nil,不会再继续查询子view的情况
touchView = nil;
}
return touchView;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return CGRectContainsPoint(self.bounds, point);
}
hitTest方法被忽略的情况
- 隐藏(hidden=YES)
- 禁止用户操作(userInteractionEnabled=NO)
- alpha小于0.01
- 如果一个子视图的区域超过父视图的bound区域,虽然该子视图内容可以显示,却无法响应事件
应用
- 让超出父view范围的子view同样可以响应事件:超出范围的子view之所以不能响应事件,是因为父视图的pointInside方法返回了NO,只要重写该方法,在方法内部判断需要响应的范围就可以了
- 同一个view,一部分范围可以响应事件,一部分范围不可以响应事件。比如要做一些不规则形状的控件,控件之间有重合的部分,需求希望只有在点击不重合的部分时才响应事件,点击重合部分不要有响应。此时可以重写该view的pointInside方法,同样在内部判断响应事件的范围
- 让一个view的子view都不能响应事件:重写该view的pointInside方法,直接返回NO
- 让一个view自身以及它的子view都不能响应事件:重写该view的hitTest方法,直接返回nil(当然有很多其他更简便的方式)