响应者链解析
当用户的手真正触摸到屏幕时,程序内部是如何响应的?实际上,当触摸到屏幕时会生成一个Touch Event(触摸事件),添加到UIApplication管理的事件队列中,UIApplication会从事件队列中依次取出事件来分发到应响应的视图去处理。当触摸事件被UIApplication发出后,会从程序的keyWindow开始,然后依次向上传递,包括各种视图控制器以及视图,最后找到合适的处理该事件的视图来响应,这整个过程就称为事件传递。
如图1-1所示,展示了几个view的层级示意图,其层级关系如下。
A为B、C的父视图,C为D、E的父视图。
当触摸视图B时,事件传递顺序为:UIApplication -> A -> B。
当触摸视图D时,事件传递顺序为:UIApplication -> A -> C -> D。
当触摸视图E时,事件传递顺序为:UIApplication -> A -> C -> E。
那么系统是根据什么来判定事件的传递顺序的呢?难道仅仅是根据子视图吗?事实上,这里涉及两个非常重要的方法。
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
这两个方法是事件传递机制的关键所在。这两个方法是UIView提供的,但并非表明只有UIView才能响应事件传递,因为除了UIView,UIViewController也可以响应事件传递的,所以它们拥有事件传递的能力取决于它们共同的父类UIResponder。
当UIApplication发送事件到keyWindow时,keyWindow会调用-hitTest:withEvent:方法来寻找最适合处理事件的视图。假设事件已经传递到某视图view,选择出能响应视图的逻辑如下:
1、首先会判断该视图自身能否处理该触摸事件,如果不能响应,则不通过pointInside方法,则hitTest方法直接返回nil;
2、如果该View可以响应,则调用-pointInside:withEvent:判断是否在显示区域上,如果不在其区域中,则返回NO,同时-hitTest:withEvent:也返回nil;
3、如果步骤2中返回YES,表示在当前View的范围中,接着先倒序遍历该视图的子视图;
4、如果步骤3中没有子视图,或者没有任何一个子视图能够响应该触摸事件,则返回该视图自身,表示只有自身可以处理该事件。
以上步骤用代码来表示的话,或者说-hitTest:withEvent:方法的原理如下:
// point是该视图的坐标系上的点
- (UIView *)hitTest:(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 *fitView = [childView hitTest:childP withEvent:event];
if (fitView) { // 寻找到最合适的view
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
视图如果满足以下三个条件其一,则不能接收触摸事件。
1、userInteractionEnabled = NO;
2、hidden = YES;
3、alpha < 0.01;
响应者链的一些实际应用
应用一、
需求: 查询某个view所在的控制器。
实现思路: 循环遍历view下一个响应者,一直找到控制器。
- (UIViewController *)findParentController:(UIResponder *)responder {
UIResponder *nextResponder = responder.nextResponder;
while (nextResponder) {
if ([nextResponder isKindOfClass:[UIViewController class]]) {
UIViewController *vc = (UIViewController *)nextResponder;
return vc;
}
nextResponder = nextResponder.nextResponder;
}
return nil;
}
应用二、
需求:比如有这么一个需求,一个大的按钮遮挡住了另一个小按钮,希望在点击这个大按钮的时候如果点击区域在小按钮的范围内就响应小按钮点击方法,否则就响应大按钮的点击方法。
实现思路:应用响应者链,判断点击范围是否在小按钮范围内,如果在则让点击事件透传到下一层,让小按钮响应。
创建一个继承与UIButton类的自定义MyButton类
MyButton.m文件
#import "MyButton.h"
// 使用全局的rect
extern CGRect rect;
@implementation MyButton
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 判断点击的点是否在小按钮区域内
BOOL isContains = CGRectContainsPoint(rect, point);
if (isContains) {
return nil;
}
return [super hitTest:point withEvent:event];
}
@end
viewController.m文件
#import "ViewController.h"
#import "MyButton.h"
// 定义一个全局的rect记录button1相对于button2的frame
CGRect rect;
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button1 = [UIButton buttonWithType:UIButtonTypeSystem];
[self.view addSubview:button1];
button1.backgroundColor = [UIColor blackColor];
button1.frame = CGRectMake(100, 100, 50, 50);
[button1 addTarget:self action:@selector(button1Action) forControlEvents:UIControlEventTouchUpInside];
MyButton *button2 = [MyButton buttonWithType:UIButtonTypeSystem];
[self.view addSubview:button2];
button2.backgroundColor = [UIColor greenColor];
button2.alpha = 0.5;
button2.frame = CGRectMake(80, 80, 150, 150);
[button2 addTarget:self action:@selector(button2Action) forControlEvents:UIControlEventTouchUpInside];
// 获取button1相对于button2的frame
rect = [button1 convertRect:button1.bounds toView:button2];
}
- (void)button1Action {
NSLog(@"button1被点击");
}
- (void)button2Action {
NSLog(@"button2被点击");
}
@end
运行效果如图2-1所示,深绿色的方块是button1 浅绿色的方块是button2, 这两个按钮在同一个父视图上,并且button2遮盖住了button1。
测试
点击深绿色位置打印 “button1被点击”
点击浅绿色部分打印 “button2被点击”
应用三、
需求: 修改一个按钮的响应范围。
实现思路: 拦截响应者链,修改响应范围。