一、事件分类
事件是发送到应用程序用于通知用户操作的对象。 在iOS中,事件可以采取多种形式:多点触摸事件,运动事件和用于控制多媒体的事件。 这最后一种类型的事件被称为遥控事件或者远程控制事件,因为它可以源自外部附件。而在我们开发过程中最常用的就是多点触摸事件。
![]()
二、事件传递
当用户生成的事件发生时,
UIKit
创建一个包含处理事件所需信息的事件对象。 然后它将事件对象放置在活动应用程序的事件队列中。 对于触摸事件,该对象是在UIEvent
对象中打包的一组触摸(UIEvent
中包含了所有UITouch
信息)。 对于运动事件,事件对象因您使用的框架和您感兴趣的运动事件类型而异。事件沿着特定路径传递,直到它被传递到可以处理它的对象。 首先,单例
UIApplication
对象从队列的顶部获取一个事件并分发处理。 通常,它将事件发送到应用程序的key window
对象,该对象将事件传递到初始对象(initial object
)进行处理。 初始对象取决于事件的类型。
触摸事件:对于触摸事件,窗口对象首先尝试将事件传递到发生触摸的视图。 该视图称为命中测试视图(
hit-test view
)。 找到命中测试视图(hit-test view
)的过程称为命中测试(hit-testing
),这在Hit-Testing返回触摸发生的视图中描述。运动和遥控事件:对于这些事件,窗口对象将摇动或远程控制事件发送到第一响应者以进行处理。 第一响应者在响应者链由响应者对象组成中描述。
这些事件路径的最终目标是找到一个可以处理和响应事件的对象。 因此,
UIKit
首先将事件发送到最适合处理事件的对象。 对于触摸事件,该对象是命中测试视图(hit-test view
),对于其他事件,该对象是第一个响应者。
三、命中测试
一根手指触摸屏幕时会创建一个
UITouch
对象,最终生成UIEvent
对象,并通过sendEvent:
函数发送给UIWindow
(keyWindow
)。
UIApplication
接收到事件,将事件传递给keyWindow
。keyWindow
遍历subViews
的hitTest:withEvent:
方法,找到点击区域内合适的视图来处理事件。UIView
的子视图也会遍历其subViews
的hitTest:withEvent:
方法,以此类推。- 直到找到点击区域内,且处于最上方的视图,将视图逐步返回给
UIApplication
。- 在查找第一响应者的过程中,已经形成了一个响应者链。
- 应用程序会先调用第一响应者处理事件。
- 如果第一响应者不能处理事件,则调用其
nextResponder
方法,一直找响应者链中能处理该事件的对象。- 然后交给
UIApplication
后,最后交给UIApplicationDelegate
,仍然没有能处理该事件的对象,则该事件被废弃。
- 这里涉及两条链:
Hit-Testing链,由系统向命中view传递:UIKit –> active app's event queue –> window –> root view –>......–>lowest view
响应链,由命中view向系统传递:initial view –> super view –> .....–> view controller –> window –> Application –> AppDelegate
![]()
举例说明:
1.如果点击UITextField
后其会成为第一响应者。
2.如果textField
未处理事件,则会将事件传递给下一级响应者链,也就是其父视图。
3.父视图未处理事件则继续向下传递,也就是UIViewController
的View
。
4.如果控制器的View
未处理事件,则会交给控制器处理。
5.控制器未处理则会交给UIWindow
。
6.然后会交给UIApplication
。
7.最后交给UIApplicationDelegate
,如果其未处理则丢弃事件。
案例说明,假设用户触摸下图中的
View E
。 iOS通过按照此顺序检查子视图来查找命中测试视图(hit-test view
):
触摸在
View A
的边界内,因此它检查子视图View B
和View C
.触摸不在
View B
的界限内,但它在View C
的界限内,因此它检查子视图View D
和View E
.触摸不在
View D
的界限内,但它在View E
的界限内。
View E
是视图层级中包含触摸的最低的视图,因此它成为命中测试视图(hit-test view
)。![]()
//模拟代码:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (self.alpha <= 0.01 || self.userInteractionEnabled == NO || self.hidden) {
return nil;
}
BOOL inside = [self pointInside:point withEvent:event];
if (inside) {
NSArray *subViews = self.subviews;
// 对子视图从上向下找
for (NSInteger i = subViews.count - 1; i >= 0; i--) {
UIView *subView = subViews[i];
CGPoint insidePoint = [self convertPoint:point toView:subView];
UIView *hitView = [subView hitTest:insidePoint withEvent:event];
if (hitView) {
return hitView;
}
}
return self;
}
return nil;
}
四、知识点应用
调用
hitTest
,获取到被点击的视图,也就是第一响应者:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
想让指定视图来响应事件,不再遍历子视图传递事件,可以通过重写hitTest
方法。
hitTest
方法内部会通过调用pointInside
,来判断点击区域是否在视图上,是则返回YES
,不是则返回NO
:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
通过重写pointInside
方法,可以将有效点击区域扩大。另外,应用程序通过响应者来接收和处理事件(能够响应事件的对象都是
UIResponder
的子类对象,例如UIView
、UIViewController
、UIApplication
等)。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。
第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder
的nextResponder
决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。

五、注意点
在遍历视图时,忽略以下三种情况的视图,如果视图具有以下特征则忽略:
- 视图的
hidden
等于YES
。- 视图的
alpha
小于等于0.01
。- 视图的
userInteractionEnabled
为NO
。但是视图的背景颜色是
clearColor
,并不在忽略范围内。
六、优先级
事件到来后先会执行
hitTest
和pointInside
操作,通过这两个方法找到第一响应者。当找到第一响应者并将其返回给UIApplication
后,UIApplication
会向第一响应者派发事件,并且遍历整个响应者链。开始会执行响应者链中的touches
系列方法。会先执行touchesBegan
和touchesMoved
方法,如果响应者链能够继续响应事件,则执行touchesEnded
方法表示事件完成。如果响应者链中有能够处理当前事件的手势,则将事件交给手势处理,调用touchesCancelled
方法将响应者链打断。
如果UIButton
(所有继承自UIControl
类)是第一响应者,则直接由UIApplication
派发事件,不通过响应者链派发。如果其不能处理事件,则交给手势处理或响应者链传递。
- 代码验证
- 自定义
TestView
重写touches
系列方法:
@implementation TestView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan TextView");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesMoved TextView");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesEnded TextView");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesCancelled TextView");
}
@end
//点击结果
touchesBegan TextView
touchesEnded TextView
给TestView
或者其父控件添加UITapGestureRecognizer
点击手势后:
//点击结果
touchesBegan TextView
tap
touchesCancelled TextView
在view
添加单击手势之后,原来的touchesEnded
方法就无效了,继而执行touchesCancelled
。
- 自定义
TestButton
重写Tracking
系列方法,并添加点击方法:
@implementation TestButton
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@"beginTracking");
return YES;
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@"continueTracking");
return YES;
}
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
NSLog(@"endTracking");
}
- (void)cancelTrackingWithEvent:(UIEvent *)event {
NSLog(@"cancelTracking");
}
//点击效果
beginTracking
endTracking
buttonToClick
给其父控件添加UITapGestureRecognizer
点击手势后:
//点击效果
beginTracking
endTracking
buttonToClick
- 优先级:系统的
UIControl
> 手势 > 自定义的UIControl
如果给TestButton
添加UITapGestureRecognizer
点击手势后:
//点击效果
beginTracking
tap
cancelTracking
- 补充:最后响应的途径便是
sendAction
分发event
到一个对象去处理:


七、补充点

-
source1
是runloop
用来处理mach port
传来的系统事件的,source0
是用来处理用户事件的。在之前Runloop执行流程中提到过source1
和source0
。
推荐参考:https://mp.weixin.qq.com/s/kkWWCb1Zy4d-lPRdPUoVHg
(文章部分来自此参考)
网友评论