事件传递与响应者链条

点击了屏幕上的一个View,事件是怎么找到这个View的呢?这个View又是怎么响应这个传递过来的事件的呢?

事件传递

当点击了屏幕上的一个View后,系统会产生一个UIEvent事件,这个事件被加入到由UIApplication管理的一个事件队列中,UIApplication会从事件队列中取出最前面的事件,然后再传递给UIWindow,最后UIWindow通过- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;在视图层次结构中找到一个最合适的view来处理触摸事件。

如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件

view 不能处理触摸事件的情况:

  1. userInteractionEnabled = NO
  2. hidden = YES
  3. alpha = 0.0~0.01(临界点待确定)

查找最合适的view来处理事件

1.自己是否能接收触摸事件?否事件传递结束
2.触摸点是否在自己身上?否事件传递结束
3.从后往前遍历子控件,重复前面的两步
4.如果没有符合条件的子控件,那么就自己最合适处理

1
2
3
4
5
//调用时机:只要有一个事件传递到控件上,就会调用控件的这个方法。point是基于方法调用者的坐标系
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;

//用来判断point这个点是否在方法调用者上,point基于方法调用者的坐标系
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

模拟系统,实现hitTest方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event
{
if(self.hidden == YES || self.alpha <= 0.01 || self.userInteractionEnabled == NO){
return nil;
}

if([self pointInside:point withEvent:event] == NO){
return nil;
}

int count = self.subviews.count;
for(int i = count-1; i >= 0; i--){
UIView *childView = self.subviews[i];
CGPoint childP = [self convertPoint:point toView:childView];
UIView *bestView = [childView hitTest:childP withEvent:event];
if(bestView){
return bestView;
}
}
return self;
}

当一个View的超出了它的父视图,点击时,是无法接受事件的,如果想让它接收事件,可以通过下面方式实现

代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//BottomView.m中

//方式一:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint scantPoint = [self convertPoint:point toView:self.scanBtn];
if ([self.scanBtn pointInside:scantPoint withEvent:event]) {
return self.scanBtn;
} else {
return [super hitTest:point withEvent:event];
}
}

//方式二:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGPoint scantPoint = [self convertPoint:point toView:self.scanBtn];
if ([self.scanBtn pointInside:scantPoint withEvent:event]) {
return YES;
}
return [super pointInside:point withEvent:event];
}

事件响应

响应者链的事件传递过程:

1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图

2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理

3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象

4>如果UIApplication也不能处理该事件或消息,则将其丢弃

总结

事件处理的整个流程总结:

1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。

3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

4.最合适的view会调用自己的touches方法处理事件

5.touches默认做法是把事件顺着响应者链条向上抛。

事件的传递和响应的区别:

  • 事件的传递是从上到下(父控件到子控件)
  • 事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)

QA

Q:增大Button的点击区域

A:

重写Button的pointInside方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%@",NSStringFromCGPoint(point));
if (point.x < 0 && fabs(point.x) > self.exInsets.left) {
return [super pointInside:point withEvent:event];
} else if(point.x > 0 && point.x-CGRectGetWidth(self.frame) > self.exInsets.right){
return [super pointInside:point withEvent:event];
} else if(point.y < 0 && fabs(point.y) > self.exInsets.top){
return [super pointInside:point withEvent:event];
} else if (point.y > 0 && point.y - CGRectGetHeight(self.frame) > self.exInsets.bottom){
return [super pointInside:point withEvent:event];
} else {
return YES;
}
}

Q: 通过View找到它所在的ViewController

A:

通过nextResponder方法找到响应链中的VC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation UIView (DM)

- (UIViewController *)findViewController:(Class)aClass
{
id responder = self;
while (responder) {
NSLog(@"%@",responder);
if ([responder isKindOfClass:aClass]) {
return responder;
}
responder = [responder nextResponder];
}
return nil;
}
@end

Demo