笔记整理:响应者链 和 第一响应者

一、概述

  • iOS中能响应事件的都必须继承于UIResponder类。
    例如UIApplication 、UIWindow、UIViewController、UIView都是继承与它。
    UIResponder用来处理和事件响应相关的事情。

  • hitTest:withEvent:机制会被调用多次,测试发现是2次。

1、UIEvent、UITouch 、UIPress、UIControl

@interface UIEvent : NSObject
@end

@interface UITouch : NSObject
@end

@interface UIPress : NSObject
@end
  • UIEvent表示具体的触摸屏幕的一次动作,比如按压、晃动、远程控制等。。

  • UITouch是与手指相关联,一根手指对应一个 UITouch 对象,保持保存着跟手指相关的信息,比如触摸类型(直接一根手指、还是用触摸笔)、触摸状态(一根手指开始接触屏幕、在屏幕上移动等等)、触摸力度、触摸事件、几个手指触摸等等。

  • UIPressUITouch 类似,表示按压的相关信息。

  • 一个事件 UIEvent 可以包含一个或多个的UITouch 或者 UIPress

  • UIControl:为视图添加绑定action而封装的,例如UIButtonUITextField

- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;

2、事件响应的相关方法 和 所在的类

  • UIView -> UIResponder
@interface UIView(UIViewGeometry)
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
@end

@interface UIView (UIViewGestureRecognizers)
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
@end
  • UIResponder
@interface UIResponder : NSObject
// 所有自定义 UITouch 的,必须重写这四个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

// 所有自定义 UIPress 的,必须重写这四个方法
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
@end

3、UIView 不能接收触摸事件情况:

  • userInteractionEnabled = NO;
  • hidden = YES;
  • alpha = 0.0~0.01;
  • 超出父控件的点击区域;

4、事件在未截断的情况下沿着响应链传递给最佳响应者,伪代码如下:

0 - [CustomView touchesBegan:withEvent
1 - [UIWindow _sendTouchesForEvent]
2 - [UIWindow sendEvent]           
3 - [UIApplication sendEvent]      
4 __dispatchPreprocessEventFromEventQueue
5 __handleEventQueueInternal
6 _CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION_
7 _CFRunLOOPDoSource0
8 _CFRunLOOPDoSources0
9 _CFRunLoopRun
10 _CFRunLoopRunSpecific
11 GSEventRunModal
12 UIApplication
13 main
14 start

// UIApplication.m
- (void)sendEvent {
  [window sendEvent];
}

// UIWindow.m
- (void)sendEvent{
  [self _sendTouchesForEvent];
}

- (void)_sendTouchesForEvent{
  //find AView Because we know hitTest View
  [AView touchesBegan:withEvent];
}

二、页面事件的传递过程

借用他人的一张图

1、用户点击屏幕,产生一个电信号;然后交由IOKit.framework 处理,封装成 IOHIDEvent 对象;
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event。

应用程序 `主线程runloop` 申请了一个 `mach port` 用于监听 `IOHIDEvent` 的 `Source1` 事件,回调方法是 `__IOHIDEventSystemClientQueueCallback()`;

回调函数内部又进一步分发 Source0 事件( Source0事件都是自定义的,非基于端口 port,包括触摸,滚动,selector选择器事件),它的回调方法是 __UIApplicationHandleEventQueue(),然后将接收到的 IOHIDEvent 事件对象封装成我们熟悉的 UIEvent 事件;

2、取出 队列最前面 的事件,'UIApplication' 调用sendEvent: 方法,向下传递事件,交给主窗口 'UIWindow' 对象处理。

3、'UIWindow' 会通过 Hit-Test机制 寻找合适的视图 'UIView' 。
UIWindow 会通过hitTest:withEvent:方法寻找触碰点所在的视图,这个过程称之为hit-test view,具体过程下一节详细介绍。

4、 找到响应事件的视图UIView,并返回该视图。

  • “从上自下” 的 事件传递过程可以概括为:UIApplication -> UIWindow -> UIViewController -> UIView -> UIButton
    响应链流程.png

三、如何找到第一响应者?即 Hit-Test机制过程

Hit-Test的过程详细说明

1、在顶级视图(RootView)上调用 pointInside:withEvent:方法判断触摸点是否在当前视图内;

2、如果返回NO,那么hitTest:withEvent: 返回 nil;如果返回YES,那么它会向 当前视图的所有子视图 发送 hitTest:withEvent: 消息。
注意:自上而下遍历:所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕。

3、如果有 子视图subviewhitTest:withEvent: 返回非空对象;则返回此对象,处理结束。
注意:这个过程,子视图也是根据 pointInside:withEvent:的返回值,来确定是返回空还是当前子视图对象的。但是这个过程中,如果子视图的hidden=YESuserInteractionEnabled=NOalpha小于0.1都会被忽略。

4、如果所有 子视图subview 遍历结束,仍然没有返回非空对象,则 hitTest:withEvent: 返回 self

借用别人的图
  • 相关过程,代码猜测说明
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
  // 1、判断触摸位置是否在当前视图内
  if ([self pointInside:point withEvent:event]) {
      NSArray<UIView *> * superViews = self.subviews;
      // 2、倒序 从最上面的一个视图开始查找
      for (NSUInteger i = superViews.count; i > 0; i--) {
          UIView * subview = superViews[i - 1];
          // 转换坐标系 使坐标基于子视图
          CGPoint newPoint = [self convertPoint:point toView:subview];

          // 得到子视图 hitTest 方法返回的值
          UIView * view = [subview hitTest:newPoint withEvent:event];

          // 3、如果子视图返回一个view 就直接返回 不在继续遍历
          if (view) {
              return view;
          }
      }

      // 4、所有子视图都没有返回 则返回自身
      return self;
  }
  return nil;
}

/**
 * 确认点击点是否在当前View范围内
 */
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
  //伪代码
  return CGRectContainsPoint(self.bounds, point);
}

四、例子验证

例子1:一个UIView添加到UIViewController.view上,然后点击该视图上面!

  • 1、点击该视图上面
/* UIView */
1、调用 hittest 碰撞方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
    return [super hitTest:point withEvent:event];
}

2、调用touch相关方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
}
/* UIView End*/
  • 2、如果添加手势后,还会调用gesture的系列方法。
3、调用手势方法
/* UIView */
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    return YES;
}
/* UIView End*/
B993E217-E9F1-4554-9816-B6EAA2832671.png
  • 3、如果点击在视图的父视图上,假设UIViewController 实现 touchesBegan:withEvent: 方法。
/* UIView */
1、调用 hittest 碰撞方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
    return [super hitTest:point withEvent:event];
}
/* UIView End*/


/* 
 * 向上依次寻找,如果 UIViewController 实现touch方法,响应则停止,否则继续往上。 
 */

/* UIViewController */
2、调用 touch 反复
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    
}
/* UIViewController End*/
C7A38A28-FC47-423D-95CE-9FAFFF1A01CB.png

例子2:Hit-Test机制

说明:设置self.viewTestView,然后依次添加 OrangeViewRedView,在OrangeView上面添加YellowView。并且UIViewController实现了touchesBegan:withEvent:方法。

image.png

- (void)loadView {
    self.view = [[TestView alloc] init];
    self.view.backgroundColor = UIColor.whiteColor;
    self.view.frame = [UIScreen mainScreen].bounds;
}
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    TestViewOrange *tv = [[TestViewOrange alloc] initWithFrame:CGRectMake(50, 50, 100, 50)];
    tv.backgroundColor = UIColor.orangeColor;
    [self.view addSubview:tv];
    
    TestViewYellow *yv = [[TestViewYellow alloc] initWithFrame:CGRectMake(5, 5, 20, 20)];
    yv.backgroundColor= UIColor.yellowColor;
    [tv addSubview:yv];
    
    TestViewRed *tvr = [[TestViewRed alloc] initWithFrame:CGRectMake(50, 150, 100, 50)];
    tvr.backgroundColor = UIColor.redColor;
    [self.view addSubview:tvr];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
}
@end



// views
@implementation TestView
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
    id view = [super hitTest:point withEvent:event];
    NSLog(@"%s %@", __func__, [view class]);
    return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    BOOL flag = [super pointInside:point withEvent:event];
    NSLog(@"\n");
    NSLog(@"%s %@", __func__, @(flag));
    return flag;
}
@end

@implementation TestViewOrange
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
    id view = [super hitTest:point withEvent:event];
    NSLog(@"%s %@", __func__, [view class]);
    return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    BOOL flag = [super pointInside:point withEvent:event];
    NSLog(@"\n");
    NSLog(@"%s %@", __func__, @(flag));
    return flag;
}
@end


@implementation TestViewYellow
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
    id view = [super hitTest:point withEvent:event];
    NSLog(@"%s %@", __func__, [view class]);
    return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    BOOL flag = [super pointInside:point withEvent:event];
    NSLog(@"\n");
    NSLog(@"%s %@", __func__, @(flag));
    return flag;
}
@end

@implementation TestViewRed
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
    id view = [super hitTest:point withEvent:event];
    NSLog(@"%s %@", __func__, [view class]);
    return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    BOOL flag = [super pointInside:point withEvent:event];
    NSLog(@"\n");
    NSLog(@"%s %@", __func__, @(flag));
    return flag;
}
@end
  • 点击self.view,非OrangeViewRedView内。
HelloWorld[14905:2268033] -[TestView pointInside:withEvent:] 1
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewRed pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewRed hitTest:withEvent:] (null)
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewOrange pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewOrange hitTest:withEvent:] (null)
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestView hitTest:withEvent:] TestView
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[ViewController touchesBegan:withEvent:]
  • 点击RedView
HelloWorld[14905:2268033] -[TestView pointInside:withEvent:] 1
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewRed pointInside:withEvent:] 1
HelloWorld[14905:2268033] -[TestViewRed hitTest:withEvent:] TestViewRed
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestView hitTest:withEvent:] TestViewRed
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[ViewController touchesBegan:withEvent:]
  • 点击OrangeView
HelloWorld[14905:2268033] -[TestView pointInside:withEvent:] 1
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewRed pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewRed hitTest:withEvent:] (null)
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewOrange pointInside:withEvent:] 1
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewYellow pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewYellow hitTest:withEvent:] (null)
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewOrange hitTest:withEvent:] TestViewOrange
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestView hitTest:withEvent:] TestViewOrange
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[ViewController touchesBegan:withEvent:]
  • 点击YellowView
HelloWorld[14905:2268033] -[TestView pointInside:withEvent:] 1
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewRed pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewRed hitTest:withEvent:] (null)
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewOrange pointInside:withEvent:] 1
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewYellow pointInside:withEvent:] 1
HelloWorld[14905:2268033] -[TestViewYellow hitTest:withEvent:] TestViewYellow
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestViewOrange hitTest:withEvent:] TestViewYellow
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[TestView hitTest:withEvent:] TestViewYellow
HelloWorld[14905:2268033] 
HelloWorld[14905:2268033] -[ViewController touchesBegan:withEvent:]

五、第一响应者

  • 什么是第一响应者?
    第一响应者是一个UIWindow对象接收到一个事件后,第一个来响应的该事件的对象。

    注意:这个第一响应者与之前讨论的触摸检测到的第一个响应的UIView并不是一个概念。
    第一响应者一般情况下用于处理非触摸事件(手机摇晃、耳机线控的远程空间)或 非本窗口的触摸事件(键盘触摸事件),通俗点讲其实就是管别人闲事的响应者。

  • 相对应的类UIResponder 和 方法

@interface UIResponder : NSObject <UIResponderStandardEditActions>
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO

// 称为第一响应者
- (BOOL)becomeFirstResponder;

@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES

// 取消第一响应者,放弃第一响应者
- (BOOL)resignFirstResponder;

// 是不是第一响应者
@property(nonatomic, readonly) BOOL isFirstResponder;
@end

六、应用场景

1、事件拦截

  • 扩大UIButton的点击响应区域。

2、事件转发

  • 最常用的场景就是让子视图超出父视图的部分也能响应事件。
    重写父视图的 pointInsidehitTest 方法,让 pointInside 返回 YES 或 让 hitTest 直接返回子视图。

  • 例子:处理 TabbarView 上面添加小红点(上面添加拖拽手势),导致红点区域点击,无法切换TabbarView。

// 让手势延迟开始响应
badge.pangesture.delaysTouchesBegan = YES;
// 让事件传递到
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if ([self.nextResponder respondsToSelector:@selector(sendActionsForControlEvents:)]) {
        [(UIControl *)self.nextResponder sendActionsForControlEvents:UIControlEventTouchDown | UIControlEventTouchUpInside];
    }
}
  • 使用 UIControl 的方法
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;
  • 使用 手势 的方法
/*
 * 默认YES,表示:手势被识别后,结束当前的Touch事件处理,一般会调用 `touchesCancelled:`,不再发送`touchesEnded:`
 * NO,手势识别后,不调用`touchesCancelled:`,发送`touchesEnded:`
 */
@property(nonatomic) BOOL cancelsTouchesInView;
/*
 * 手势识别后,是否阻塞调用`touchesBegan:`
 * 默认NO,不拦截,调用
 * YES,拦截,不调用
 */
@property(nonatomic) BOOL delaysTouchesBegan;

/*
 * 手势识别后,是否阻塞调用`touchesEnd:`
 * 默认YES,拦截,不调用
 * NO,不拦截,调用
 */
@property(nonatomic) BOOL delaysTouchesEnded;

参考

iOS响应者链、事件的传递

image.png

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

推荐阅读更多精彩内容