事件的产生
当我们的手指触摸屏幕,就会产生一个触摸事件,UIApplication会将该事件加入到其管理的事件队列中,队列是先进先出(FIFO)的,也符合先产生的事件优先处理的逻辑.UIApplication会取出最前面的事件,并将事件分发下去进行处理.
事件的传递
UIApplication会将事件发送给程序的主窗口,主窗口根据其视图层级结构中(图层树)找到最合适的视图来处理该事件.
所以事件的传递是从父视图到子视图的
其中涉及的主要方法为hitTest:withEvent(返回最合适的视图来处理事件),其内部会调用pointInside:withEvent:(判断触摸点是否在当前视图范围内)
主要逻辑
主窗口在它的内容视图上调用hitTest:withEvent:来找寻最合适处理触摸事件的view.
hitTest:withEvent:在内部首先会判断该视图是否能响应触摸事件,如果不能响应,返回nil,表示该视图不响应此触摸事件。然后再调用pointInside:withEvent:判断点击事件发生的位置是否处于当前视图范围内。如果pointInside:withEvent:返回NO,那么hiteTest:withEvent:也直接返回nil。
如果pointInside:withEvent:返回YES,则向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历。直到有子视图返回非空对象或者全部子视图遍历完毕;若第一次有子视图返回非空对象,则 hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回nil,则hitTest:withEvent:方法返回该视图自身。
不接收触摸事件的三种情况
(1)不接收用户交互 userInteractionEnabled = NO(2)隐藏 hidden = YES(3)透明 alpha = 0.0 ~ 0.01
hitTest:withEvent:底层实现
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判断自己能否接收触摸事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断触摸点在不在自己范围内,point是该视图的坐标系上的点
if (![self pointInside:point withEvent:event]) return nil;
// 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件
int count = self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
//转换为子视图的坐标位置
CGPoint childPoint = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childPoint withEvent:event];
if (fitView) {
return fitView;
}
}
// 没有找到比自己更合适的view
return self;
}
每个能执行hitTest:方法的view都属于事件传递的一部分,但是,只有pointInside返回YES的view才属于响应者链条。
事件的响应
响应者:继承UIResponder的对象称之为响应者对象,能够处理touchesBegan等触摸事件,加速器事件,远程控制等。响应者链条:由很多响应者链接在一起组合起来的一个链条称之为响应者链条.
通过事件传递找到最合适的处理触摸事件的view后(就是最后一个pointInside返回YES的view,它是第一响应者),如果该view是控制器view,那么上一个响应者就是控制器。如果它不是控制器view,那么上一个响应者就是前面一个pointInside返回YES的view(其实就是它的父控件)。 最后这些所有pointInside返回YES的view加上它们的控制器、UIWindow和UIApplication共同构成响应者链条。响应者链条是从子控件到父控件的(上到下),事件的传递是(父控件到子控件)自下而上的。
应用
- 超出父控件依然能够响应事件
- 扩大按钮的点击范围
- 获取view所属的控制器
超出父控件依然能够响应事件
在父视图中重写hitTest:withEvent:
//重写父视图的hitTest方法,遍历子视图,如果子视图的bounds在点击范围内就返回这个子视图,即这个子视图就能响应这个事件
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
if (view == nil) {
for (UIView *subView in self.subviews) {
CGPoint myPoint = [subView convertPoint:point fromView:self];
if (CGRectContainsPoint(subView.bounds, myPoint)) {
return subView;
}
}
}
return view;
}
扩大按钮的点击范围
在需要扩大的视图中重写pointInside:withEvent:方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
//1.获取原来的bounds
CGRect bounds = self.bounds;
int insetOffsert = -20;
//2.CGRect CGRectInset(CGRect rect, CGFloat dx, CGFloat dy)
//以原rect为中心,再参考dx,dy,进行缩放或者放大。 +缩小,-放大,可以理解为内边距,两边都有边距,所以取1/2
bounds = CGRectInset(bounds, 0.5 * insetOffsert, 0.5 *insetOffsert);
//3.判断扩大后的区域是否包含点击坐标
return CGRectContainsPoint(bounds, point);
}
获取view所属的控制器
//可以获取到父容器的控制器的方法
- (UIViewController *)View:(UIView *)view{
UIResponder *responder = view;
//循环获取下一个响应者,直到响应者是一个UIViewController类的一个对象为止,然后返回该对象.
while ((responder = [responder nextResponder])) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController *)responder;
}
}
return nil;
}
网友评论