UI

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

2021-07-07  本文已影响0人  双鱼子曰1987

一、概述

1、UIEvent、UITouch 、UIPress、UIControl

@interface UIEvent : NSObject
@end

@interface UITouch : NSObject
@end

@interface UIPress : NSObject
@end
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;

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

@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
@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 不能接收触摸事件情况:

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,并返回该视图。

5、然后,事件是沿着响应者链,从下往上。

响应者链可以分为 “事件传递” 和 “事件响应”。

三、如何找到第一响应者?即 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上,然后点击该视图上面!

/* 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*/
3、调用手势方法
/* UIView */
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    return YES;
}
/* UIView End*/
B993E217-E9F1-4554-9816-B6EAA2832671.png
/* 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
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:]
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:]
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:]
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:]

五、第一响应者

@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、事件拦截

2、事件转发

// 让手势延迟开始响应
badge.pangesture.delaysTouchesBegan = YES;
// 让事件传递到
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if ([self.nextResponder respondsToSelector:@selector(sendActionsForControlEvents:)]) {
        [(UIControl *)self.nextResponder sendActionsForControlEvents:UIControlEventTouchDown | UIControlEventTouchUpInside];
    }
}
- (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响应者链、事件的传递

上一篇下一篇

猜你喜欢

热点阅读