响应者链

说响应者链之前,先说一个方法- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event,这个方法的作用是用来判断,操作的触摸点是否在当前视图上。返回YES:触摸点在当前视图上,视图会响应事件。返回NO:触摸点不在当前视图上,视图不会响应事件。重写这个方法让它返回NO,我们在点击到viewA上的时候,viewA不会响应,其父视图会响应我们操作。这样就做到了穿透viewA对viewB进行操作。还可以可以根据参数point来判断,进行点击区域的选择,可以实现,一个视图View,前半部分能响应点击,后半部分不响应点击。


然后我们再说一下UIResponder,它响应用户的操作处理各种事件。UIView,UIViewController, UIApplication都是继承于它。而UIWindow,UILabel,UIImageView是继承于UIView。所以他们都能成为响应者,成为响应者链的一环。
当一个触摸事件产生后,系统会分为两步来处理:事件的传递+事件的响应。

事件的传递

当触摸屏幕的时候,系统是这样传递这个触摸事件的:

  • 加入到一个由UIApplication管理的事件队列中(队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列中)
  • UIApplication会发送事件给应用程序的主窗口keyWindow。
  • 从主窗口keyWindow开始遍历subViews的hitTest:withEvent方法,也就是会调用hitTest:withEvent:方法在视图层级结构中逐级遍历子视图,并且遍历的过程中不断判断视图的范围,并最终找到第一响应者(如果子视图不在点击区域或者没有子视图,则当前视图就是第一响应者).
  • 此时已经找到点击区域内,且处于最上方的视图,将视图逐步返回给UIApplication。在查找第一响应者的过程中,已经形成了一个响应者链,应用程序会先调用第一响应者处理事件。如果第一响应者不能处理事件,则调用其nextResponder方法,一直找响应者链中能处理该事件的对象。最后到UIApplication后仍然没有能处理该事件的对象,则该事件被废弃。

最主要的是下面这个方法

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ 
}

其工作流程是:

  • 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内
  • 若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil
  • 若pointInside:withEvent:方法返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图(subviews),调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕。
  • 若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束
  • 若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身(self)
    在忽略以下三种情况的视图,如果视图具有以下特征则忽略。但是视图的背景颜色是clearColor,并不在忽略范围内。
  1. 视图的hidden等于YES。
  2. 视图的alpha小于等于0.01。
  3. 视图的userInteractionEnabled为NO。

总结;每个view都有这个方法,用来处理用户的操作事件。它返回:self,代表这个view会接受用户的操作事件,返回:nil,则代表这个view不会接受用户的操作事件。

事件的响应

经过以上的事件的传递过程,事件已经传递给系统认为最适合的View了。接下来就是处理这个事件。
处理事件方法:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
}

但是最适合的,不一定就能一定能处理,如果,这个View不能处理这个事件则会将这个事件上抛,就是按照事件传递下来的路线上抛。
大致流程如下:

  • 不能处理将事件传递给其上级视图(View的superView);
  • 如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller;
  • 首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传递;
  • 一直到 window,如果window还是不能处理此事件则继续交给application处理;
  • 如果最后application还是不能处理此事件则将其丢弃
  • 如果父控件不能接受事件, 那么子控件就不能接受事件.

事件拦截

有时候想让指定视图来响应事件,不再向其子视图继续传递事件,可以通过重写hitTest:withEvent:方法。在执行到方法后,直接将该视图返回,而不再继续遍历子视图,这样响应者链的终端就是当前视图。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return self;
}

事件转发

在开发过程中,经常会遇到子视图显示范围超出父视图的情况,这时候可以重写该视图的pointInside:withEvent:方法,将点击区域扩大到能够覆盖所有子视图。

还有一点需要注意,响应者链和手势同时出现时,也就是既实现了touches方法又添加了手势,会发现touches方法有时会失效,这是因为手势的执行优先级是高于响应者链的。

事件到来后先会执行hitTest和pointInside操作,通过这两个方法找到第一响应者,这个在上面已经详细讲过了。当找到第一响应者并将其返回给UIApplication后,UIApplication会向第一响应者派发事件,并且遍历整个响应者链。如果响应者链中能够处理当前事件的手势,则将事件交给手势处理,并调用touches的cancelled方法将响应者链取消。

在UIApplication向第一响应者派发事件,并且遍历响应者链查找手势时,会开始执行响应者链中的touches系列方法。会先执行touchesBegan和touchesMoved方法,如果响应者链能够继续响应事件,则执行touchesEnded方法表示事件完成,如果将事件交给手势处理则调用touchesCancelled方法将响应者链打断。

根据苹果的官方文档,手势不参与响应者链传递事件,但是也通过hitTest的方式查找响应的视图,手势和响应者链一样都需要通过hitTest方法来确定响应者链的。在UIApplication向响应者链派发消息时,只要响应者链中存在能够处理事件的手势,则手势响应事件,如果手势不在响应者链中则不能处理事件。

你可能感兴趣的:(响应者链)