iOS开发响应者链

响应者链解析

当用户的手真正触摸到屏幕时,程序内部是如何响应的?实际上,当触摸到屏幕时会生成一个Touch Event(触摸事件),添加到UIApplication管理的事件队列中,UIApplication会从事件队列中依次取出事件来分发到应响应的视图去处理。当触摸事件被UIApplication发出后,会从程序的keyWindow开始,然后依次向上传递,包括各种视图控制器以及视图,最后找到合适的处理该事件的视图来响应,这整个过程就称为事件传递。

图1-1.png

如图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。

图2-1.png

测试
点击深绿色位置打印 “button1被点击”
点击浅绿色部分打印 “button2被点击”

应用三、

需求: 修改一个按钮的响应范围。
实现思路: 拦截响应者链,修改响应范围。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355