参考文章Understanding Event Handling, Responders, and the Responder Chain。
响应链工作原理
Touch events 是 UIView
常见的点击事件,当手指触摸屏幕上某一控件到响应相关事件分为两部分:
- 事件的传递
- 事件的响应
事件的传递涉及了 UIView
里面的两个方法:
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//判断当前点击事件是否存在最优响应者
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// default returns YES if point is in bounds
//判断当前点击坐标是否在点击控件的 bounds 之内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
事件的传递流程
- 触碰屏幕产生
UIEvent
事件并放入UIApplication
事件队列中,然后在整个视图结构中自上而下分发。 -
UIWindow
接收到事件开始逆序遍历subviews
查找最优响应视图。
如果
hitTest
&pointInside
查找到了最优响应视图则后续遍历subviews
的操作就会终止。
视图查找代码示例:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//如果视图 alpha < 0.01,userInteractionEnabled = NO,hidden = YES,返回 nil
if (self.alpha < 0.01 || !self.userInteractionEnabled || self.hidden) return nil;
//point 是否在当前视图 bounds 之内,如果不在返回 nil
if (![self pointInside:point withEvent:event]) return nil;
/*
typedef NS_OPTIONS(NSUInteger, NSEnumerationOptions) {
NSEnumerationConcurrent = (1UL << 0), //当前排序
NSEnumerationReverse = (1UL << 1), //倒序
};
*/
__block UIView *hitView = nil;
[self.subviews enumerateObjectsWithOptions:0 usingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
hitView = [obj hitTest:point withEvent:event];
if (hitView) *stop = YES;
}];
return hitView ? hitView : self;
}
关于 UIView
逆序遍历 subviews
查找最优响应视图可以用 Method Swizzling
方法交换 hitTest
方法打印查看。
#import "UIView+HitTest.h"
#import
@implementation UIView (HitTest)
+ (void)load {
Class class = [self class];
SEL oriSel = @selector(hitTest:withEvent:);
SEL swiSel = @selector(_hitTest:withEvent:);
Method oriMethod = class_getInstanceMethod(class, oriSel);
Method swiMethod = class_getInstanceMethod(class, swiSel);
BOOL isAddMethod = class_addMethod(class, oriSel, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (isAddMethod) {
class_replaceMethod(class, swiSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
}
- (UIView *)_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"%@ %s",[self class],__PRETTY_FUNCTION__);
return [self _hitTest:point withEvent:event];
}
这里新建三个 View
,View1
、View2
、View3
并按照顺序加载在 ViewController
上。
依次点击 View1
、View2
、View3
:
//View1
2018-01-02 18:24:53.441840+0800 Response[8173:228607] UIWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.447251+0800 Response[8173:228607] UIView -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.447697+0800 Response[8173:228607] View3 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.447947+0800 Response[8173:228607] View2 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.448151+0800 Response[8173:228607] View1 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.448385+0800 Response[8173:228607] UIStatusBarWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:24:53.448513+0800 Response[8173:228607] UIStatusBar_Modern -[UIView(HitTest) _hitTest:withEvent:]
//View2
2018-01-02 18:25:21.644110+0800 Response[8173:228607] UIWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.646276+0800 Response[8173:228607] UIView -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.646478+0800 Response[8173:228607] View3 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.646709+0800 Response[8173:228607] View2 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.646848+0800 Response[8173:228607] UIStatusBarWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:21.647004+0800 Response[8173:228607] UIStatusBar_Modern -[UIView(HitTest) _hitTest:withEvent:]
//View3
2018-01-02 18:25:35.386537+0800 Response[8173:228607] UIWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:35.387783+0800 Response[8173:228607] UIView -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:35.388519+0800 Response[8173:228607] View3 -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:35.388639+0800 Response[8173:228607] UIStatusBarWindow -[UIView(HitTest) _hitTest:withEvent:]
2018-01-02 18:25:35.388736+0800 Response[8173:228607] UIStatusBar_Modern -[UIView(HitTest) _hitTest:withEvent:]
事件的响应流程
- 确定最优响应视图,并判断其能否响应事件,如果可以响应则响应链传递终止,反之传递给
nextResponder
(通常为superview
)。 - 如果事件传递至
UIWindow
且无法响应,则事件传递给UIApplication
,如果UIApplication
也无法响应,最终事件被抛弃。
并不是所有的
nextResponder
都是其superview
,比如UIViewController
的根视图self.view
的nextResponder
就是其所在UIViewController
。而如果UIViewController
是UIWindow
的rootViewController
,那么它的nextResponder
就是UIWindow
,但如果UIViewController
是present
出来的,那么它的nextResponder
就是之前所执行present
操作的那个UIViewController
。
总结
Touch event
在 Responder Chain
中的传递与响应:
- 事件的分发和传递是自上而下的
- 事件的响应是自下而上的