(四)事件的响应及在响应链中的传递

本文系转载,原文地址为iOS触摸事件全家桶

经历Hit-Testing后,UIApplication已经知道事件的最佳响应者是谁了,接下来要做的事情就是:

  1. 将事件传递给最佳响应者响应
  2. 事件沿着响应链传递

事件响应的前奏

因为最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。首先,UIApplication将事件通过 sendEvent: 传递给事件所属的window,window同样通过 sendEvent: 再将事件传递给hit-tested view,即最佳响应者。过程如下:

UIApplication ——> UIWindow ——> hit-tested view

以寻找事件的最佳响应者一节中点击视图E为例,在EView的 touchesBegan:withEvent: 上断点查看调用栈就能看清这一过程:

image

那么问题又来了。这个过程中,假如应用中存在多个window对象,UIApplication是怎么知道要把事件传给哪个window的?window又是怎么知道哪个视图才是最佳响应者的呢?

其实简单思考一下,这两个过程都是传递事件的过程,涉及的方法都是 sendEvent: ,而该方法的参数(UIEvent对象)是唯一贯穿整个经过的线索,那么就可以大胆猜测必然是该触摸事件对象上绑定了这些信息。事实上之前在介绍UITouch的时候就说过touch对象保存了触摸所属的window及view,而event对象又绑定了touch对象,如此一来,是不是就说得通了。要是不信的话,那就自定义一个Window类,重写 sendEvent: 方法,捕捉该方法调用时参数event的状态,答案就显而易见了。

image

至于这两个属性是什么时候绑定到touch对象上的,必然是在hit-testing的过程中呗,仔细想想hit-testing干的不就是这个事儿吗~

事件的响应

前面介绍UIResponder的时候说过,每个响应者必定都是UIResponder对象,通过4个响应触摸事件的方法来响应事件。每个UIResponder对象默认都已经实现了这4个方法,但是默认不对事件做任何处理,单纯只是将事件沿着响应链传递。若要截获事件进行自定义的响应操作,就要重写相关的方法。例如,通过重写 touchesMoved: withEvent: 方法实现简单的视图拖动。

- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;

每个响应触摸事件的方法都会接收两个参数,分别对应触摸对象集合事件对象。通过监听触摸对象中保存的触摸点位置的变动,可以时时修改视图的位置。视图(UIView)作为响应者对象,本身已经实现了 touchesMoved: withEvent:方法,因此要创建一个自定义视图(继承自UIView),重写该方法。

//MovedView
//重写touchesMoved方法(触摸滑动过程中持续调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    //获取触摸对象
    UITouch *touch = [touches anyObject];
    //获取前一个触摸点位置
    CGPoint prePoint = [touch previousLocationInView:self];
    //获取当前触摸点位置
    CGPoint curPoint = [touch locationInView:self];
    //计算偏移量
    CGFloat offsetX = curPoint.x - prePoint.x;
    CGFloat offsetY = curPoint.y - prePoint.y;
    //相对之前的位置偏移视图
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

每个响应者都有权决定是否执行对事件的响应,只要重写相关的触摸事件方法即可。

事件的传递(响应链)

前面一直在提最佳响应者,之所以称之为“最佳”,是因为其具备响应事件的最高优先权(响应链顶端的男人)。最佳响应者首先接收到事件,然后便拥有了对事件的绝对控制权:即它可以选择独吞这个事件,也可以将这个事件往下传递给其他响应者,这个由响应者构成的视图链就称之为响应链。

需要注意的是,上一节中也说到了事件的传递,与此处所说的事件的传递有本质区别。上一节所说的事件传递的目的是为了寻找事件的最佳响应者,是自下而上的传递;而这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下的。前者为“寻找”,后者为“响应”。

响应者对于事件的操作方式:

响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent: 方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。

响应者对于接收到的事件有3种操作:

  • 不拦截,默认操作
    事件会自动沿着默认的响应链往下传递
  • 拦截,不再往下分发事件
    重写 touchesBegan:withEvent: 进行事件处理,不调用父类的 touchesBegan:withEvent:
  • 拦截,继续往下分发事件
    重写 touchesBegan:withEvent: 进行事件处理,同时调用父类的 touchesBegan:withEvent: 将事件往下传递

响应链中的事件传递规则:

每一个响应者对象(UIResponder对象)都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者。因此,一旦事件的最佳响应者确定了,这个事件所处的响应链就确定了。

对于响应者对象,默认的 nextResponder 实现如下:

  • UIView
    若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder为父视图。
  • UIViewController
    若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。
  • UIWindow
    nextResponder为UIApplication对象。
  • UIApplication
    若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。
image

上图是官网对于响应链的示例展示,若触摸发生在UITextField上,则事件的传递顺序是:

  • UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegation

图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponder为UIViewController对象;若是直接add在UIWindow上的,则其nextResponder为UIWindow对象。

可以用以下方式打印一个响应链中的每一个响应对象,在最佳响应者的 touchBegin:withEvent: 方法中调用即可(别忘了调用父类的方法)

- (void)printResponderChain
{
    UIResponder *responder = self;
    printf("%s",[NSStringFromClass([responder class]) UTF8String]);
    while (responder.nextResponder) {
        responder = responder.nextResponder;
        printf(" --> %s",[NSStringFromClass([responder class]) UTF8String]);
    }
}

以上一节原型按钮的案例为例,重写CircleButton的 touchBegin:withEvent:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self printResponderChain];
    [super touchesBegan:touches withEvent:event];
}

点击原型按钮的任意区域,打印出的完整响应链如下:

CircleButton --> CustomeTabBar --> UIView --> UIViewController --> UIViewControllerWrapperView --> UINavigationTransitionView --> UILayoutContainerView --> UINavigationController --> UIWindow --> UIApplication --> AppDelegate

另外如果有需要,完全可以重写响应者的 nextResponder 方法来自定义响应链。

下一篇 (五)举个栗子

你可能感兴趣的:((四)事件的响应及在响应链中的传递)