【iOS】—— 响应者链和事件传递链

响应者链和事件传递链

二者概念

响应链

  • 由离用户最近的view向系统传递。
  • initial view –> super view –> …… –> view controller –> window –> Application –> AppDelegate

传递链

  • 由系统向离用户最近的view传递。
  • UIKit –> active app's event queue –> window –> root view –> …… –> lowest view

请添加图片描述

响应者

响应者链中的各个对象被称为“响应者”(responder),响应者链的根节点是UIApplication对象,所有的事件都从UIApplication对象开始传递。当事件发生时,UIApplication对象首先将其传递给当前显示在屏幕上的UIWindow对象,然后递归地向下传递给其子视图,依次传递给UIViewControllerUIView等响应者对象进行处理,直到事件被处理完毕或被丢弃。

在响应者链中,每个响应者对象都可以处理事件,也可以选择将事件传递给下一个响应者对象进行处理,或者直接丢弃事件。响应者链中的每个响应者对象都可以重写几个方法来处理事件,这些方法包括touchesBegan:withEvent:touchesMoved:withEvent:touchesEnded:withEvent:等等。

响应链事件

请添加图片描述
iOS中的事件类型:

  • 触摸事件(手机在屏幕上触摸)
  • 加速计事件(手机摇一摇)
  • 远程遥控事件(遥控器控制)

UIKit继承图

请添加图片描述

通过继承图我们能知道,我们平时在使用的UI大多数都是继承自UIResponder的,只有继承自UIResponder的对象才能接收并处理事件,我们把这类对象称为“响应者”。就像UIApplication,UIViewController,UIView都继承自UIResponder,因此他们都可以接收处理事件。

UIResponder

UIResponder中提供了三种处理事件的方法(触摸事件、加速计事件、远程控制事件),所以我们才能在UI中实现各种点击事件:

// 触摸事件
// 开始接触屏幕,就会调用一次
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
// 手指开始移动就会调用(这个方法会频繁的调用,其实一接触屏幕就会多次调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
// 手指离开屏幕时,调用一次
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者view上面添加手势时,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

// 加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

// 远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

通过这三种处理事件的方法就可以知道事件的整个过程,我们平时经常使用的就是触摸事件,其中有两个参数,UITouch和UIEvent。

UITouch

当你用一根手指触摸屏幕时,会创建一个与之关联的UITouch对象,一个UITouch对象对应一根手指。在事件中可以根据NSSet中UITouch对象的数量得出此次触摸事件是单指触摸还是双指多指等等。

UITouch几个重要属性
// 触摸产生时所处的窗口
@property(nonatomic, readonly, retain) UIWindow *window;
// 触摸产生时所处的视图
@property(nonatomic, readonly, retain) UIView *view;
// 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic, readonly) NSUInteger tapCount;
// 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic, readonly) NSTimeInterval timestamp;
// 当前触摸事件所处的状态
@property(nonatomic, readonly) UITouchPhase phase;

UITouch的两个方法(可用于view的拖拽)

- (CGPoint)locationInView:(UIView *)view;
/*
  返回值表示触摸在view上的位置
  这里返回的位置是针对传入的view的坐标系(以view的左上角为原点(0, 0))
  调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
*/

// 该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(UIView *)view;

UIEvent

每产生一个事件,就会产生一个UIEvent事件,UIEvent称为事件对象,记录事件产生的时刻和类型等等。

UIEvent几个重要属性

// 事件类型
@property(nonatomic, readonly) UIEventType type;
@property(nonatomic, readonly) UIEventSubtype subtype;
// 事件产生的时间
@property(nonatomic, readonly) NSTimeInterval timestamp;

事件的产生与传递

传递链

UIApplication传递事件到当前Window是明确的(即一定会的),接下来就是从Window开始找最佳响应视图,此过程有两个重要的方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // 递归调用的事件,从最底层开始,一直往上找,直到找到一个最上层的能响应的视图就返回该视图
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // 判断点击区域是否在该视图的范围中,不在该视图范围中就结束递归,返回nil

传递过程

  • 1.发生触摸事件后,压力转为电信号,系统将产生UIEvent事件,记录事件产生的时间和类型。
  • 2.系统会将该事件添加到UIApplication管理的事件队列中。
  • 3.UIApplication将事件队列中的第一个事件分发给UIWindow,这时就会调用UIWindow的hitTest:withEvent:方法。
  • 4.当前 window/视图 调用hitTest:withEvent:方法,hitTest:withEvent:方法内部会通过以下条件判断 window/视图 能否能响应事件,以下判断条件都是不能响应事件的:
    • 不允许交互:userInteractionEnabled=NO
    • 隐藏:hidden = YES
    • 透明度:alpha < 0.01,alpha小于0.01为全透明
  • 5.如果能响应,该函数又会调用pointInside方法判断当前触摸点是不是在视图范围内,不在视图范围内也是不会响应的。
  • 6.如果在 window/视图 范围内,开始反向遍历 window/视图 的子视图列表subviews,遍历的同时会调用subviews中每个子视图的hitTest:withEvent:方法,判断逻辑和上面的一样,直到找到离用户最近的、能响应事件的视图。
  • 4.5.6过程会递归判断,直到找到最外层合适的view,最后返回的view就是最佳响应视图。

hitTest:withEvent:方法的可能实现

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    
    
    // 1.判断当前控件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha) 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;
}
  • 查找结束,返回最终的view,UIApplication会调用UIWindow的sendEvent,从而触发对应的响应方法,如果我们在UIWindow中重写sendEvent而不调用super的实现,所有的点击事件都不会触发,因为事件是从最底层传递上来的,你切断了最底层的传递,肯定就无法响应了
  • 实际调用hitTest的过程,系统为了找到精准的触摸点会多次调用hitTest方法
  • 如果重写hitTest返回self,传递过程就会终止,那么当前view就是最合适的view;返回nil,传递也会终止,父视图superView就是最合适的view
  • 如果遍历subviews的过程都没找到合适的view,那么subviews中的子view的hitTest方法都会被调用一次
  • hitTest方法会调用pointInside判断当前视图是否在点击区域,所以超出父视图边界的控件无法响应事件
  • 同一个view上的两个子视图有重叠部分,后加入的视图会被加入到事件传递链
  • 在打印视图层级结构中部分视图执行hitTestpointInSide方法中可以看到,viewController并没有执行这两个方法。所以传递链中没有viewController,因为viewController本身不具有大小的概念。而响应链中有viewController,因为viewController继承UIResponder。

响应链

当找到最合适的响应者之后,便会调用控件相应的touch方法来作具体处理,然而这些方法默认都是不做处理的,但是我们要是想让该响应者响应该事件就可以重写一开始说的那几个响应事件方法,并且我们也可以在重写touch方法中加入[super touch],使多个响应者同时响应同一事件。如果我们对响应事件的方法不做处理那么将该事件随着响应者链条往回传递,交给上一个响应者来处理(即调用super的touch方法),直到找到一个能响应该事件的响应者。

响应过程

  • 1.通过hitTest返回的view为当前事件的第一响应者,nextResponder为上一个响应者
  • 2.如果当前view默认不去重写响应事件方法,或者重写调用了父类的响应事件方法,响应就会沿着响应者链向上传递(上一个响应者一般是superView,可以通过nextResponder属性获取上一个响应者)
  • 3.如果上一个响应者是viewController,由viewController的view处理,如果view本身没处理,则传递给viewController本身
  • 4.重复上述过程,直到传递到window,window如果也不能处理,则传递到UIApplication,如果UIApplication的delegate继承自UIResponder,则交给delegate处理,如果delegate也不处理最后丢弃

完整的触摸过程

一次完整的触摸过程,会经历 3 个状态。 上面说了完整的响应事件有如下四个事件:

触摸开始:- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
触摸移动:- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
触摸结束:- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
触摸取消(可能会经历):- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
完整的触摸过程如何实现

一个完整的触摸事件流程通常包括以下几个步骤:

  • 手指触摸到屏幕,系统会创建一个与手指相关联的 UITouch 对象,并将其加入到系统中的事件队列中。
    系统会将该事件发送给当前 UIWindow 对象,即调用 UIWindow 对象的 touchesBegan(_:with:) 方法,并将该事件传递给子视图。
  • 从根视图开始,系统会通过递归调用 hitTest(_:with:) 方法,寻找响应该事件的视图。在每个视图中,系统都会调用point(inside:with:) 方法,判断该视图是否包含该事件的触摸点。
  • 一旦找到了响应该事件的视图,系统会将该事件发送给该视图,即调用该视图的touchesBegan(:with:)方法。
    在该视图的 touchesBegan(:with:) 方法中,开发者可以对该事件做出相应的处理,比如更改视图的状态、更新视图的内容等。
  • 如果该事件需要传递给其它视图进行处理,开发者可以手动调用 next 方法,将该事件传递给下一个响应者。
  • 当手指离开屏幕时,系统会将一个 touch 对象的 phase 属性设置为 .ended,并将该 touch 对象从事件队列中移除。
    当前的 UIWindow 对象会将该事件发送给响应者链中的下一个响应者。如果没有下一个响应者,则该事件的响应过程结束。

举例

  • 4 个触摸事件处理方法中,都有 NSSet *touches 和 UIEvent *event 两个参数。
  • 一次完整的触摸过程中,只会产生一个事件对象,4 个触摸方法都是同一个 event 参数。
  • 如果两根手指同时触摸一个 view,那么 view 只会调用一次 touchesBegan:withEvent: 方法,touches 参数中装着 2 个 UITouch 对象。
  • 如果这两根手指一前一后分开触摸同一个 view,那么 view 会分别调用 2 次 touchesBegan:withEvent: 方法,并且每次调用时的 touches 参数中只包含一个 UITouch 对象。
  • 根据 touches 中 UITouch 的个数可以判断出是单点触摸还是多点触,判断多少次点击:UITouch 的属性 @property(nonatomic,readonly) NSUInteger tapCount;
  • 事件的产生和传递:
    • 发生触摸事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中;
    • UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先发送事件给应用程序的主窗口(keyWindow);
    • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,但是这仅仅是整个事件处理过程的第一步;
    • 找到合适的视图控件后,就会调用视图控件的 touches 方法来作具体的事件处理
      请添加图片描述
      UIView 不接收触摸事件的三种情况:
  • 不接收用户交互userInteractionEnabled = NO;
  • 隐藏 hidden = YES;
  • 透明 alpha = 0.0 ~ 0.01。
    UIImageView 的 userInteractionEnabled 默认就是 NO,因此 UIImageView 以及它的子控件默认是不能接收触摸事件的。

iOS的响应者链的传递过程和触摸事件的传递过程一样吗

先触发响应者事件,接着触发触摸事件,也就是先寻找命中者.
实际上,触摸事件的传递确实是从父控件到子控件,而响应者链的传递是从子控件到父控件,两者的传递方向是相反的

下面是触摸事件的传递过程:

  • 用户触摸屏幕,系统将创建一个 UITouch 对象,并将其加入到一个 UIEvent 对象中。
  • UIEvent 对象传递给 UIApplication 对象,由 UIApplication 对象将事件分派给应用程序的 UIWindow
  • UIWindow 对象将事件分派给合适的视图控制器的视图层次结构。事件会从根视图开始沿着视图层次结构向下传递,直到找到合适的视图来响应事件。
  • 视图响应事件,并将事件传递给其父视图,直到事件到达视图层次结构的最上层视图。
  • 如果事件没有被任何视图对象处理,则系统将发送一个 cancel 事件,表示触摸事件已被取消。

而响应者链的传递过程则是从子控件到父控件,如下:

  • 视图层次结构中的子控件可以成为响应者对象,当用户与子控件交互时,该控件将成为响应者对象。
  • 当响应者对象无法处理事件时,它将将事件发送给其父控件,并在响应者链中继续向上传递。
  • 如果父控件也无法处理事件,则将事件发送到祖先控件,直到到达视图层次结构中的最上层控件。
  • 如果没有控件能够处理事件,则事件将被丢弃。

响应规则定义区别

先触发响应者事件,接着触发触摸事件
触摸事件传递过程是从父控件到子控件,即由UIApplication将事件发送到最顶层的控件,然后由这个控件向下逐级传递事件,直到找到最合适的处理者为止。在传递过程中,如果一个控件能够响应事件,那么就会处理事件并结束传递;如果不能响应事件,那么就会将事件传递给下一个响应者。

而响应者链的传递过程则是从子控件到父控件。当一个控件接收到事件后,它首先会将事件交给自己的响应者对象处理,然后再交给它的父控件的响应者对象处理,直到到达最顶层的响应者对象为止。在传递过程中,如果一个响应者对象能够响应事件,那么就会处理事件并结束传递;如果不能响应事件,那么就会将事件传递给它的父响应者对象。

因此,虽然两个传递过程都涉及到父子控件之间的传递,但它们的传递顺序和目的不同。触摸事件传递过程主要是为了找到最合适的控件来处理事件,而响应者链传递过程则是为了让控件的父子关系中的响应者对象能够逐级处理事件。

过程区别

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

  • 如果 view 的控制器存在,就传递给控制器;如果控制器不存在,则将其传递给它的父视图;
  • 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给 window 对象进行处理;
  • 如果 window 对象也不处理,则其将事件或消息传递给 UIApplication 对象;
  • 如果 UIApplication 也不能处理该事件或消息,则将其丢弃。

触摸事件处理的详细过程:

  • 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件;
  • 找到最合适的视图控件后,就会调用控件的 touches 方法来作具体的事件处理;
  • 这些 touches 方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理。响应者链条就是由多个响应者对象连接起来的链条,能很清楚地看见每个响应者之间的关系,并且可以让一个事件多个对象处理;

如何判断上一个响应者:

  • 如果当前这个 view 是控制器的 view,那么控制器就是上一个响应者;
  • 如果当前这个 view 不是控制器的 view,那么父控件就是上一个响应者。

事件传递的完整过程:

  • 先将事件对象由上往下传递(由父控件传递给子控件),找到最合适的控件来处理这个事件;
  • 调用最合适控件的 touches…. 方法;
  • 如果调用 [super touches….] 就会将事件顺着响应者链条往上传递,传递给上一个响应者;
  • 接着就会调用上一个响应者的 touches…. 方法。

总结

iOS的响应者链机制的步骤是先通过hitTest和PointSide方法找到合适的控件-Initial View如果这个响应者能够响应
则进行

Initial View -> View Controller(如果存在) -> superview -> · ··  -> rootView -> UIWindow -> UIApplication

如果一个View有一个视图控制器(View Controller),它的下一个响应者是这个视图控制器,紧接着才是它的父视图(Super View),如果一直到Root View都没有处理这个事件,事件会传递到UIWindow(iOS中有一个单例Window),此时Window如果也没有处理事件,便进入UIApplication,UIApplication是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环。

这块内容不算难,也比较好理解,但是好多博客都没有讲清楚主要问题,推荐看我疼组的博客:我是道哥

猜你喜欢

转载自blog.csdn.net/m0_62386635/article/details/130297367