iOS UI事件传递与响应者链
响应者链
- 响应者对象:继承自UIResponder的对象称之为响应者对象。UIApplication、UIWindow、UIViewController和所有继承UIView的UIKit类都直接或间接的继承自UIResponder。
UIResponder一般响应以下几种事件:触摸事件(touch handling)、点按事件(press handling)、加速事件和远程控制事件:
触摸事件(touch handling)
- (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)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
点按事件(press handling) NS_AVAILABLE_IOS(9_0)
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
加速事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
远程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
-
响应者链:由多个响应者组合起来的链条,就叫做响应者链。它表示了每个响应者之间的联系,并且可以使得一个事件可选择多个对象处理
响应者链.png
假设触摸了initial view,
1.第一响应者就是initial view即initial view首先响应touchesBegan:withEvent:方法,接着传递给橘黄色的view
2.橘黄色的view开始响应touchesBegan:withEvent:方法,接着传递给蓝绿色view
3.蓝绿色view响应touchesBegan:withEvent:方法,接着传递给控制器的view
4.控制器view响应touchesBegan:withEvent:方法,控制器传递给了窗口
5.窗口再传递给application
如果上述响应者都不处理该事件,那么事件被丢弃
事件的产生和传递
当一个触摸事件产生的时候,我们的程序是如何找到第一响应者的呢?
事件的产生传递.png
当你点击了屏幕会产生一个触摸事件,消息循环(runloop)会接收到触摸事件放到消息队列里,UIApplication会会从消息队列里取事件分发下去,首先传给UIWindow,UIWindow会使用hitTest:withEvent:方法找到此次触摸事件初始点所在的视图,找到这个视图之后他就会调用视图的touchesBegan:withEvent:方法来处理事件。
-
hitTest:withEvent:查找过程
hitTestview过程.png
图片中view等级
[ViewA addSubview:ViewB];
[ViewA addSubview:ViewC];
[ViewB addSubview:ViewD];
[ViewB addSubview:ViewE];
点击viewE:
1.A 是UIWindow的根视图,首先对A进行hitTest:withEvent:
2.判断A的userInteractionEnabled,如果为NO,A的hitTest:withEvent返回nil;
3.pointInside:withEvent:方法判断用户点击是否在A的范围内,显然返回YES
4.遍历A的子视图B和C,由于从后向前遍历,
- 因此先查看C,调用C的hitTest:withEvent方法:pointInside:withEvent:方法判断用户点击是否在C的范围内,不在返回NO,C对应的hitTest:withEvent: 方法return nil;
- 再查看B,调用B的hitTest:withEvent方法:pointInside:withEvent:判断用户点击是否在B的返回内,在返回YES
遍历B的子视图D和E,从后向前遍历,
先查看E,调用E的hitTest:withEvent方法:pointInside:withEvent:方法 判断用户点击是否在E的范围内,在返回YES,E没有子视图,因此E对应的hitTest:withEvent方法返回E,再往前回溯,就是B的hitTest:withEvent方法返回E,因此A的hitTest:withEvent方法返回E。
至此,点击事件的第一响应者就找到了。
事件流程.png如果hitTest:withEvent: 找到的第一响应者view没有处理该事件,那么事件会沿着响应者链向上传递->父视图->视图控制器,如果传递到最顶级视图还没处理事件,那么就传递给UIWindow处理,若window对象也不处理->交给UIApplication处理,如果UIApplication对象还不处理,就丢弃该事件。
注意:控件不能响应的情况
1.userInteractionEnabled = NO
2.hidden = YES
3.透明度 alpha 小于等于0.01
4.子视图超出了父视图区域
子视图超出父视图,不响应的原因:因为父视图的pointInside:withEvent:方法返回了NO,就不会遍历子视图了。可以重写pointInside:withEvent:方法解决此问题。