iOS 事件传递和响应机制

2,259 阅读6分钟
原文链接: www.jianshu.com

本文主要参考了 VV木公子(简书作者)史上最详细的iOS之事件的传递和响应机制
我按照自己的理解做了排版和一些表述的修改。

在开发过程中我们经常会遇到一些事件响应优先级的问题,通过搜索知道了 hitTest,再根据 hitTest 去搜索一些类似问题,问题最终是解决了,但是我们得知道为什么是这么解决的。以下内容就是详细说明 iOS 的事件传递和响应机制。

(一)iOS 中的事件

iOS 的事件分为3大类型:

  1. 触摸事件
  2. 加速计事件
  3. 远程控制事件

本篇只讨论触摸事件

1.1 响应者对象(UIResponder)

在 iOS 中不是任何对象都能处理事件,只有继承了 UIResponder 的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自 UIResponder 的,所以都能接收并处理事件。

  • UIApplication
  • UIViewController
  • UIView

UIViewController 可以通过在.m中覆写相关方法来处理事件,例如 touchesBegan、touchesMoved 等;UIView 则必须通过自定义子类来处理。

(二)iOS 中事件的产生和传递

2.1 事件的产生

  1. 发生触摸事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中。为什么是队列而不是栈?因为队列的特点是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
  2. UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先发送事件给应用程序的主窗口(keyWindow)。
  3. 主窗口(keyWindow)会在视图层次结构中找到一个最合适的视图来处理触摸事件,寻找最合适的视图的关键就是 hitTest:withEvent: 方法。

2.2 事件的传递

事件的传递是自上到下的顺序,即 UIApplication->window->处理事件最合适的 view。

  1. 首先判断主窗口(keyWindow)自己是否能接受触摸事件
  2. 判断触摸点是否在自己身上
  3. 子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
  4. 通过前3步寻找到了 fitView,那么会把这个事件交给这个 fitView,再遍历这个 fitView 的子控件,直至没有更合适的 view 为止。
  5. 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的 view。

上述流程的实现代码如下所示:

- (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.从后往前遍历子控件数组
    int count = (int)self.subviews.count;
    for (int 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;
        }
    }
    // 4.没有找到更合适的view,也就是没有比自己更合适的view
    return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//{
// return NO;
//}

知道了 hitTest 的作用,我们可以通过覆写它来达到干涉事件的传递。例如 whiteView 上有两个 view,一个 redView,一个 greenView,redView 先添加到父视图上,系统默认将事件传递给后添加的 view 也就是 greenView上,如果我们想让 redView 响应事件可通过覆写 whiteView 的 hitTest 方法指定返回 redView,再在 redView 的 touch 处理方法中执行就达到了事件拦截的目的。

UIView 不能接收触摸事件的三种情况:

  1. 不允许交互:userInteractionEnabled = NO
  2. 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
  3. 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。< p="">

     注意:默认 UIImageView 不能接受触摸事件,因为不允许交互,即 userInteractionEnabled = NO,所以如果希望 UIImageView 可以交互,需要 userInteractionEnabled = YES。

(三)iOS 中事件的响应

在 iOS 程序中无论是最后面的 UIWindow 还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在 iOS 中响应者链的关系可以用下图表示:


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

  1. 如果当前 view 是控制器的 view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前 view 不是控制器的 view,那么父视图就是当前 view 的上一个响应者,事件就传递给它的父视图。
  2. 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给 window 对象进行处理。
  3. 如果 window 对象也不处理,则其将事件或消息传递给 UIApplication 对象。
  4. 如果 UIApplication 也不能处理该事件或消息,则将其丢弃。

(四)iOS 事件传递和响应总结:

  1. 当一个事件发生后,事件会从父控件传给子控件,也就是说由 UIApplication -> UIWindow -> UIView -> fit view,以上就是事件的传递,也就是寻找最合适的view的过程。
  2. 接下来是事件的响应。首先看 fit view 能否处理这个事件,如果不能则会将事件传递给其上级视图(fit view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器 view controller,首先判断视图控制器的根视图 view 是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果 window 还是不能处理此事件则继续交给 application 处理,如果最后 application 还是不能处理此事件则将其丢弃。
  3. 在事件的响应中,如果某个控件实现了 touches...方法,则这个事件将由该控件来接受,如果调用了 [super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的 touches….方法。

如果您觉得本文对您有所帮助,请点击「喜欢」来支持我。

转载请注明出处,有任何疑问都可联系我,欢迎探讨。