一、概述
iOS中能响应事件的都必须继承于
UIResponder
类。
例如UIApplication 、UIWindow、UIViewController、UIView
都是继承与它。
UIResponder
用来处理和事件响应相关的事情。hitTest:withEvent:
机制会被调用多次,测试发现是2次。
1、UIEvent、UITouch 、UIPress、UIControl
@interface UIEvent : NSObject
@end
@interface UITouch : NSObject
@end
@interface UIPress : NSObject
@end
UIEvent
表示具体的触摸屏幕的一次动作,比如按压、晃动、远程控制等。。UITouch
是与手指相关联,一根手指对应一个UITouch
对象,保持保存着跟手指相关的信息,比如触摸类型(直接一根手指、还是用触摸笔)、触摸状态(一根手指开始接触屏幕、在屏幕上移动等等)、触摸力度、触摸事件、几个手指触摸等等。UIPress
与UITouch
类似,表示按压的相关信息。一个事件
UIEvent
可以包含一个或多个的UITouch
或者UIPress
。UIControl:为视图添加绑定action而封装的,例如
UIButton
、UITextField
等
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
2、事件响应的相关方法 和 所在的类
UIView -> UIResponder
@interface UIView(UIViewGeometry)
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
@end
@interface UIView (UIViewGestureRecognizers)
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
@end
UIResponder
@interface UIResponder : NSObject
// 所有自定义 UITouch 的,必须重写这四个方法
- (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;
// 所有自定义 UIPress 的,必须重写这四个方法
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
@end
3、UIView 不能接收触摸事件情况:
- userInteractionEnabled = NO;
- hidden = YES;
- alpha = 0.0~0.01;
- 超出父控件的点击区域;
4、事件在未截断的情况下沿着响应链传递给最佳响应者,伪代码如下:
0 - [CustomView touchesBegan:withEvent
1 - [UIWindow _sendTouchesForEvent]
2 - [UIWindow sendEvent]
3 - [UIApplication sendEvent]
4 __dispatchPreprocessEventFromEventQueue
5 __handleEventQueueInternal
6 _CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION_
7 _CFRunLOOPDoSource0
8 _CFRunLOOPDoSources0
9 _CFRunLoopRun
10 _CFRunLoopRunSpecific
11 GSEventRunModal
12 UIApplication
13 main
14 start
// UIApplication.m
- (void)sendEvent {
[window sendEvent];
}
// UIWindow.m
- (void)sendEvent{
[self _sendTouchesForEvent];
}
- (void)_sendTouchesForEvent{
//find AView Because we know hitTest View
[AView touchesBegan:withEvent];
}
二、页面事件的传递过程
1、用户点击屏幕,产生一个电信号;然后交由IOKit.framework
处理,封装成 IOHIDEvent
对象;
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework
生成一个 IOHIDEvent
事件并由 SpringBoard
接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event。
应用程序 `主线程runloop` 申请了一个 `mach port` 用于监听 `IOHIDEvent` 的 `Source1` 事件,回调方法是 `__IOHIDEventSystemClientQueueCallback()`;
回调函数内部又进一步分发 Source0
事件( Source0
事件都是自定义的,非基于端口 port
,包括触摸,滚动,selector选择器事件),它的回调方法是 __UIApplicationHandleEventQueue()
,然后将接收到的 IOHIDEvent
事件对象封装成我们熟悉的 UIEvent
事件;
2、取出 队列最前面 的事件,'UIApplication' 调用sendEvent:
方法,向下传递事件,交给主窗口 'UIWindow' 对象处理。
3、'UIWindow' 会通过 Hit-Test机制
寻找合适的视图 'UIView' 。
UIWindow
会通过hitTest:withEvent:
方法寻找触碰点所在的视图,这个过程称之为hit-test view
,具体过程下一节详细介绍。
4、 找到响应事件的视图UIView
,并返回该视图。
- “从上自下” 的 事件传递过程可以概括为:
UIApplication -> UIWindow -> UIViewController -> UIView -> UIButton
三、如何找到第一响应者?即 Hit-Test
机制过程
Hit-Test
的过程详细说明
1、在顶级视图(RootView)上调用 pointInside:withEvent:
方法判断触摸点是否在当前视图内;
2、如果返回NO
,那么hitTest:withEvent:
返回 nil
;如果返回YES
,那么它会向 当前视图的所有子视图 发送 hitTest:withEvent:
消息。
注意:自上而下遍历:所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕。
3、如果有 子视图subview
的 hitTest:withEvent:
返回非空对象;则返回此对象,处理结束。
注意:这个过程,子视图也是根据 pointInside:withEvent:
的返回值,来确定是返回空还是当前子视图对象的。但是这个过程中,如果子视图的hidden=YES
、userInteractionEnabled=NO
、alpha小于0.1
都会被忽略。
4、如果所有 子视图subview
遍历结束,仍然没有返回非空对象,则 hitTest:withEvent:
返回 self
;
- 相关过程,代码猜测说明
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
// 1、判断触摸位置是否在当前视图内
if ([self pointInside:point withEvent:event]) {
NSArray<UIView *> * superViews = self.subviews;
// 2、倒序 从最上面的一个视图开始查找
for (NSUInteger i = superViews.count; i > 0; i--) {
UIView * subview = superViews[i - 1];
// 转换坐标系 使坐标基于子视图
CGPoint newPoint = [self convertPoint:point toView:subview];
// 得到子视图 hitTest 方法返回的值
UIView * view = [subview hitTest:newPoint withEvent:event];
// 3、如果子视图返回一个view 就直接返回 不在继续遍历
if (view) {
return view;
}
}
// 4、所有子视图都没有返回 则返回自身
return self;
}
return nil;
}
/**
* 确认点击点是否在当前View范围内
*/
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
//伪代码
return CGRectContainsPoint(self.bounds, point);
}
四、例子验证
例子1:一个UIView添加到UIViewController.view上,然后点击该视图上面!
- 1、点击该视图上面
/* UIView */
1、调用 hittest 碰撞方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
return [super hitTest:point withEvent:event];
}
2、调用touch相关方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
}
/* UIView End*/
- 2、如果添加手势后,还会调用
gesture
的系列方法。
3、调用手势方法
/* UIView */
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
return YES;
}
/* UIView End*/
- 3、如果点击在视图的父视图上,假设
UIViewController
实现touchesBegan:withEvent:
方法。
/* UIView */
1、调用 hittest 碰撞方法
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
return [super hitTest:point withEvent:event];
}
/* UIView End*/
/*
* 向上依次寻找,如果 UIViewController 实现touch方法,响应则停止,否则继续往上。
*/
/* UIViewController */
2、调用 touch 反复
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
}
/* UIViewController End*/
例子2:Hit-Test
机制
说明:设置self.view
为TestView
,然后依次添加 OrangeView
和 RedView
,在OrangeView
上面添加YellowView
。并且UIViewController
实现了touchesBegan:withEvent:
方法。
- (void)loadView {
self.view = [[TestView alloc] init];
self.view.backgroundColor = UIColor.whiteColor;
self.view.frame = [UIScreen mainScreen].bounds;
}
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
TestViewOrange *tv = [[TestViewOrange alloc] initWithFrame:CGRectMake(50, 50, 100, 50)];
tv.backgroundColor = UIColor.orangeColor;
[self.view addSubview:tv];
TestViewYellow *yv = [[TestViewYellow alloc] initWithFrame:CGRectMake(5, 5, 20, 20)];
yv.backgroundColor= UIColor.yellowColor;
[tv addSubview:yv];
TestViewRed *tvr = [[TestViewRed alloc] initWithFrame:CGRectMake(50, 150, 100, 50)];
tvr.backgroundColor = UIColor.redColor;
[self.view addSubview:tvr];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s", __func__);
}
@end
// views
@implementation TestView
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
id view = [super hitTest:point withEvent:event];
NSLog(@"%s %@", __func__, [view class]);
return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
BOOL flag = [super pointInside:point withEvent:event];
NSLog(@"\n");
NSLog(@"%s %@", __func__, @(flag));
return flag;
}
@end
@implementation TestViewOrange
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
id view = [super hitTest:point withEvent:event];
NSLog(@"%s %@", __func__, [view class]);
return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
BOOL flag = [super pointInside:point withEvent:event];
NSLog(@"\n");
NSLog(@"%s %@", __func__, @(flag));
return flag;
}
@end
@implementation TestViewYellow
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
id view = [super hitTest:point withEvent:event];
NSLog(@"%s %@", __func__, [view class]);
return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
BOOL flag = [super pointInside:point withEvent:event];
NSLog(@"\n");
NSLog(@"%s %@", __func__, @(flag));
return flag;
}
@end
@implementation TestViewRed
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
id view = [super hitTest:point withEvent:event];
NSLog(@"%s %@", __func__, [view class]);
return view;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
BOOL flag = [super pointInside:point withEvent:event];
NSLog(@"\n");
NSLog(@"%s %@", __func__, @(flag));
return flag;
}
@end
- 点击
self.view
,非OrangeView
和RedView
内。
HelloWorld[14905:2268033] -[TestView pointInside:withEvent:] 1
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewRed pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewRed hitTest:withEvent:] (null)
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewOrange pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewOrange hitTest:withEvent:] (null)
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestView hitTest:withEvent:] TestView
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[ViewController touchesBegan:withEvent:]
- 点击
RedView
内
HelloWorld[14905:2268033] -[TestView pointInside:withEvent:] 1
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewRed pointInside:withEvent:] 1
HelloWorld[14905:2268033] -[TestViewRed hitTest:withEvent:] TestViewRed
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestView hitTest:withEvent:] TestViewRed
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[ViewController touchesBegan:withEvent:]
- 点击
OrangeView
内
HelloWorld[14905:2268033] -[TestView pointInside:withEvent:] 1
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewRed pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewRed hitTest:withEvent:] (null)
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewOrange pointInside:withEvent:] 1
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewYellow pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewYellow hitTest:withEvent:] (null)
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewOrange hitTest:withEvent:] TestViewOrange
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestView hitTest:withEvent:] TestViewOrange
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[ViewController touchesBegan:withEvent:]
- 点击
YellowView
内
HelloWorld[14905:2268033] -[TestView pointInside:withEvent:] 1
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewRed pointInside:withEvent:] 0
HelloWorld[14905:2268033] -[TestViewRed hitTest:withEvent:] (null)
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewOrange pointInside:withEvent:] 1
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewYellow pointInside:withEvent:] 1
HelloWorld[14905:2268033] -[TestViewYellow hitTest:withEvent:] TestViewYellow
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestViewOrange hitTest:withEvent:] TestViewYellow
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[TestView hitTest:withEvent:] TestViewYellow
HelloWorld[14905:2268033]
HelloWorld[14905:2268033] -[ViewController touchesBegan:withEvent:]
五、第一响应者
-
什么是第一响应者?
第一响应者是一个UIWindow
对象接收到一个事件后,第一个来响应的该事件的对象。注意:这个第一响应者与之前讨论的触摸检测到的第一个响应的UIView并不是一个概念。
第一响应者一般情况下用于处理非触摸事件(手机摇晃、耳机线控的远程空间)或 非本窗口的触摸事件(键盘触摸事件),通俗点讲其实就是管别人闲事的响应者。 相对应的类
UIResponder
和 方法
@interface UIResponder : NSObject <UIResponderStandardEditActions>
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO
// 称为第一响应者
- (BOOL)becomeFirstResponder;
@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES
// 取消第一响应者,放弃第一响应者
- (BOOL)resignFirstResponder;
// 是不是第一响应者
@property(nonatomic, readonly) BOOL isFirstResponder;
@end
六、应用场景
1、事件拦截
- 扩大UIButton的点击响应区域。
2、事件转发
最常用的场景就是让子视图超出父视图的部分也能响应事件。
重写父视图的pointInside
或hitTest
方法,让pointInside
返回 YES 或 让hitTest
直接返回子视图。-
例子:处理 TabbarView 上面添加小红点(上面添加拖拽手势),导致红点区域点击,无法切换TabbarView。
// 让手势延迟开始响应
badge.pangesture.delaysTouchesBegan = YES;
// 让事件传递到
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if ([self.nextResponder respondsToSelector:@selector(sendActionsForControlEvents:)]) {
[(UIControl *)self.nextResponder sendActionsForControlEvents:UIControlEventTouchDown | UIControlEventTouchUpInside];
}
}
- 使用
UIControl
的方法
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;
- 使用 手势 的方法
/*
* 默认YES,表示:手势被识别后,结束当前的Touch事件处理,一般会调用 `touchesCancelled:`,不再发送`touchesEnded:`
* NO,手势识别后,不调用`touchesCancelled:`,发送`touchesEnded:`
*/
@property(nonatomic) BOOL cancelsTouchesInView;
/*
* 手势识别后,是否阻塞调用`touchesBegan:`
* 默认NO,不拦截,调用
* YES,拦截,不调用
*/
@property(nonatomic) BOOL delaysTouchesBegan;
/*
* 手势识别后,是否阻塞调用`touchesEnd:`
* 默认YES,拦截,不调用
* NO,不拦截,调用
*/
@property(nonatomic) BOOL delaysTouchesEnded;