iOS事件传递与视图响应链

首先要先学习下响应者对象UIResponder,只有继承UIResponder的的类,才能处理事件。

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIApplication : UIResponder

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>

@interface CALayer : NSObject <NSSecureCoding, CAMediaTiming>

我们可以看出UIApplication,UIView,UIViewController都是继承自UIResponder类,可以响应和处理事件。CALayer不是UIResponder的子类,无法处理事件。

1、继承UIResponder的类 都会有这些方法

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

这些方法如果我们没有重写这个方法,那么这么方法就会

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//默认super这些方法
[super touchesBegan:touches withEvent:event];
}

如果我们重写这些方法了 并且没有写

[super touchesBegan:touches withEvent:event];

那么它就不会继续向上传递。例如子视图重写此方法并没有写[super touchesBegan:touches withEvent:event]; 那么就不会触发父视图的touches方法。
2、只有继承UIView的视图类才能重写

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds
image.png

当用户触摸(Touch)屏幕进行交互时,系统首先要找到响应者(Responder)。系统检测到手指触摸(Touch)操作时,将Touch 以UIEvent的方式加入UIApplication事件队列中。UIApplication从事件队列中取出最新的触摸事件进行分发传递到UIWindow进行处理。UIWindow 会通过hitTest:withEvent:方法寻找触碰点所在的视图,这个过程称之为hit-test view。
hitTest 的顺序如下

UIApplication -> UIWindow -> Root View -> ··· -> subview

在顶级视图(Root View)上调用pointInside:withEvent:方法判断触摸点是否在当前视图内;
如果返回NO,那么hitTest:withEvent:返回nil;
如果返回YES,那么它会向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕。
如果有subview的hitTest:withEvent:返回非空对象则A返回此对象,处理结束(注意这个过程,子视图也是根据pointInside:withEvent:的返回值来确定是返回空还是当前子视图对象的。并且这个过程中如果子视图的hidden=YES、userInteractionEnabled=NO或者alpha小于0.1都会并忽略);

如果所有subview遍历结束仍然没有返回非空对象,则hitTest:withEvent:返回self;

系统就是这样通过hit test找到触碰到的视图(Initial View)进行响应。

以触摸屏幕为例,我们根据图2来说一下APP如何确认触摸方法哪一个视图上,当我们触摸屏幕时系统检测到手指触摸(Touch)操作时,将Touch 以UIEvent的方式加入UIApplication事件队列中。UIApplication从事件队列中取出最新的触摸事件进行分发传递到UIWindow进行处理。UIWindow 会通过hitTest:withEvent:方法寻找触碰点所在的视图,这个过程称之为hit-test view。

总结

Hit-Test是针对View去定位响应的View,而如果响应的View不做处理,那么就会开始走响应链,根据响应链responder chain去进行处理。

根据图2 假设我们点击了7-1 我们可以得到hit-test从1->2->3->4->10(由于hit-test返回nil 不会遍历子视图)->5->6->7->9(由于hit-test返回nil 9-1不会响应)->8(由于hit-test返回nil 8-1不会响应)->7->7-1
如果响应视图7-1没有做处理 那么就会根据响应链依次寻找处理着。

问题一

假设视图7有处理时间的代码 视图7-1没有 如果我在7-1视图重写了touchesBegan方法并且没有写[super touchesBegan...]方法,那么视图7会处理此事件么?
答:会处理的,此时是这链条上的所有手势识别器都会先于所绑定的view按一定次序开始出发状态机,不是依次等待上一个识别器有结果之后出发下一个,而且即使我们屏蔽了自定义view中touches方法,就是不调用super,那么手势识别器一样会触发action,也就是说view里面的touches方法并不影响手势的识别和事件的分发,屏蔽这个测试大家可以自己试一下
那么咱们看看官方文档是如何总结的

In the simple case, when a touch occurs, the touch object is passed from the UIApplication object to the UIWindow object. Then, the window first sends touches to any gesture recognizers attached the view where the touches occurred (or to that view’s superviews), before it passes the touch to the view object itself.

Gesture Recognizers Get the First Opportunity to Recognize a Touch
A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. During the delay, if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.

以上这些内容可以解释手势识别器的优先级是比所绑定的视图高的,而且也不是所有的touch都会传递到view.

图2.png

推荐阅读更多精彩内容