响应链相关知识点

术语

       hit-testing          点击检测

       responder chain   响应链


响应链可以做到的事情

通过相应链,我们可以改变用户操作的响应顺序

可以查找到视图的响应控制器



事件流程

当用户触发的一个事件发生,UIKit会创建一个包含要处理的事件信息的事件对象。然后她会将事件对象放入active app’s(应用程序对象,每个程序对应唯一一个)事件队列(为什么是队列而不是栈?因为队列是先进先出,用于保证先产生的事件先处理,栈是先进后出)。对于触摸事件,事件对象就是UIEvent对象封装的一系列触摸集合。对于动作事件,这个事件对象依赖于使用的framework和你关心哪种动作事件。

事件通过特殊的路径传递直到被传递到一个可以处理该事件的对象。首先,单例的UIApplication对象从顶层的队列中获取事件,然后分发。典型的,它将事件发送到App的关键window(key window)对象,window则为了处理该事件而发送它到初始化对象(initial object),这个初始化对像依靠事件类型。

初始化对象查找后的反馈过程如下图

响应链相关知识点_第1张图片

For the app on the left, the event follows this path:

The initial view attempts to handle the event or message. If it can’t handle the event, it passes the event to its superview, because the initial view is not the top most view in its view controller’s view hierarchy.

The superview attempts to handle the event. If the superview can’t handle the event, it passes the event to its superview, because it is still not the top most view in the view hierarchy.

The topmost view in the view controller’s view hierarchy attempts to handle the event. If the topmost view can’t handle the event, it passes the event to its view controller.

The view controller attempts to handle the event, and if it can’t, passes the event to the window.

If the window object can’t handle the event, it passes the event to the singleton app object.

If the app object can’t handle the event, it discards the event.

The app on the right follows a slightly different path, but all event delivery paths follow these heuristics:

A view passes an event up its view controller’s view hierarchy until it reaches the topmost view.

The topmost view passes the event to its view controller.

The view controller passes the event to its topmost view’s superview.

Steps 1-3 repeat until the event reaches the root view controller.

The root view controller passes the event to the window object.

The window passes the event to the app object.

左边的情况,最初的视图接收到事件,如果它不能处理,那么它将会传递给它的父试图,它的父试图也不能处理,再传递给父试图的父试图,如果还不能处理,再传递给ViewController,也不能处理,传递给window,也不能处理,传递给Application。

右边的情况,最初的视图它不能处理时会将事件传递给它的视图控制器,视图控制器会将事件会将事件传递给视图控制器的视图的父试图,然后重复这样的过程,直到根视图控制器,也不能处理,传递给window,也不能处理,传递给Application

苹果说两种app设置的原因,但是基本都是如果初始化对象(initial object)—— 即hit-test view或者first responder —— 不处理事件,UIKit会将事件传递给responder chain的下一个responder。每个responder决定它是传递事件还是通过nextResponder方法传递给它的下一个responder。这个操作继续直到一个responder处理event或者没有responder了。

总而言之:

事件的传递和响应分两个链:

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

响应链:由离用户最近的view向系统传递。initial view –> super view –> …..–> view controller –> window –> Application(如果view是控制器的view,就传递给控制器;如不是,则将其传递给它的父视图 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理 如果window对象也不处理,则其将事件或消息传递给UIApplication对象 如果UIApplication也不能处理该事件或消息,则将其丢弃)

上面是原理,下面说方法:

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

此方法会在视图层级结构中的每个视图上调用

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent * _Nullable)event

如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是hit-test view。

hitTest:withEvent:方法的处理流程如下:

首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;

若返回NO,则hitTest:withEvent:返回nil;

若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;

若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;

如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。

hitTest:withEvent:忽略的三种情况:

1:hidden = YES 的视图

2:userInteractionEnabled = YES 的视图

3:alpha < 0.01 的视图

如果一个子视图的区域超过父视图的bound区域(父视图的clipsToBounds 属性为NO,这样超过父视图bound区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的pointInside:withEvent:方法会返回NO,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:方法来处理这种情况。(objc_setAssociatedObject添加属性,扩展响应区域)

如果父视图需要对对哪个子视图可以响应触摸事件做特殊控制,则可以重写hitTest:withEvent:或pointInside:withEvent:方法。

如button,scrollview同为topView的子视图,但scrollview覆盖在button之上,这样在在button上的触摸操作返回的hit-test view为scrollview,button无法响应,可以修改topView的hitTest:withEvent:方法如下:

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

      UIView *result = [super hitTest:point withEvent:event];

      CGPoint buttonPoint = [underButton convertPoint:point fromView:self];

      if ([underButton pointInside:buttonPoint withEvent:event]) {

          return underButton;

       }

      return result;

你可能感兴趣的:(响应链相关知识点)