iOS触摸事件响应原理
系统响应阶段
- 1.手指触摸屏幕,屏幕感受到触摸后,将事件交给
IOKit
来处理。 - 2.
iOKit
将触摸事件封装成iOHIDEvent
对象,并通过mach port
传递给SpringBoard
进程。
mach port
是进程端口,各进程间通过它来通信。Springboard
是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统收到的触摸事件。
- 3.
Springboard
由于接收到触摸事件,因此触发了系统进程的主线程的runloop
的source
回掉。发生触摸事件的时候,你有可能正在桌面上翻页,也有可能正在头条上看新闻,如果是前者,则触发Springboard
主线程的runloop
的source0
回调,将桌面系统交给系统进程去消耗。而如果是后者,则将触摸事件通过IPC
传递给前台app
进程,后面的事便是APP
内部对于触摸事件的响应了。
APP响应触摸事件
-
1.
APP
进程的mach port
接收来自Springboard
的触摸事件,主线程runloop
被唤醒,触发source1
回调。 -
2.
source1
回调又触发了一个source0
回调,将接收到的IOHIDEvent
对象封装成UIEven
t对象,此时APP
将正式开始对于触摸的响应。 -
3.
source0
回调将触摸事件添加到UIApplication
的事件队列,当触摸事件出队后UIApplication
为触摸事件寻找最佳响应者。 -
4.寻找到最佳响应着之后,接下来的事情便是事件在响应链中传递和响应。
触摸 事件 响应者
触摸
触摸对象即UITouch
对象。一个手指触摸屏幕,就会生成一个UITouch
对象,如果多个手指同时触摸,就会生成多个UITouch
对象。多个手指先后触摸,如果系统判断多个手指触摸的是同一个地方,那么不会生成多个UITouch
对象,而是更新这个UITouch
对象,改变其tap count
。如果对歌手指触摸的不是同一个地方,那么就会产生对个UITouch
对象。
触摸事件
触摸事件即UIEvent
。UIEvent
即对UITouch
的一次封装。由于一次触摸事件并不止有一个触摸对象,可能是多指同时触摸。触摸对象集合可以通过allUITouchs
属性来获取。
响应者
响应者即UIResponser
,UIView
,UIViewController
,UIApplication
,UIAppdelegate
等实例都是UIResponser
,响应者响应触摸事件是由下面的方法来实现的:
//手指触碰屏幕,触摸开始
- (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;
寻找最佳响应者
当APP
通过mach port
得到这个触摸事件时,APP
中有那么多UIView
或者UIViewController
,到底应该谁去响应呢?寻找最佳响应者就是找出这个优先级最高的响应对象。
- 寻找最佳响应着的具体流程如下:
- 1.
UIApplication
首先将事件传递给窗口对象(UIWindow
),如果有多个UIWindow
对象,则先选择最后加上的UIWindow
对象。 - 2.若
UIWindow
对象能响应这个触摸事件,则继续向其子视图传递,向子视图传递时也是先传递给最后加上的子视图。 - 若子视图无法响应该事件,则返回父视图,再传递给倒数第二个加入该父视图的子视图。
视图如何判断自己能否响应触摸事件?
以下情况不能响应触摸事件:
- 1.触摸点不在视图范围内。
- 2.不允许交互: 视图的
userInteractionEnabled = NO
。 - 3.隐藏:
hidden = YES
,如果视图隐藏了,则不能响应事件。 - 4.透明度:当视图的透明度小于等于0.01时,不能响应事件。
寻找最佳响应者的原理
hitTest:withEvent:
每个UIView
都有一个hitTest:witnEvent:
方法。这个方法是寻找最佳响应者的核心方法,同时又是传递事件的桥梁。它的作用是询问事件在当前视图中的响应者。hitTest:withEvent:
返回一个UIView
对象,作为当前视图层次中的响应者。其默认实现是:
- 若当前视图无法响应事件,则返回nil。
- 若当前视图能响应事件,但无子视图可响应事件,则返回当前视图。
- 若当前视图能响应事件,同时有子视图能响应,则返回子视图层次中的事件响应者。
开始时UIApplication
调用UIWindow
的hitTest:wuithEvent:
方法将触摸事件传递给UIWindow
,如果UIWindow
能够响应触摸事件,则调用hitTest:withEvent:
将事件传递给其子是视图并询问子视图上的最佳响应者,这样一级一级传递下去,获取最终的最佳响应者。
hitTest:withEvent:
的代码实现大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3种状态无法响应事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//从后往前遍历子视图数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--)
{
// 获取子视图
UIView *childView = self.subviews[i];
// 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
CGPoint childP = [self convertPoint:point toView:childView];
//询问子视图层级中的最佳响应视图
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView)
{
//如果子视图中有更合适的就返回
return fitView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}
注意这里的方法pointInside:withEvent:
,这个方法是判断触摸点是否在视图范围内。默认的实现是如果触摸点在视图范围内则返回YES
,否则返回NO
。
下面我们在上图中的每个视图层次中添加三个方法来验证之前的分析:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super hitTest:point withEvent:event];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super pointInside:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
点击视图,打印出来的结果是:
-[AView hitTest:withEvent:]
-[AView pointInside:withEvent:]
-[CView hitTest:withEvent:]
-[CView pointInside:withEvent:]
-[EView hitTest:withEvent:]
-[EView pointInside:withEvent:]
-[EView touchesBegan:withEvent:]
这和我们的分析是一致的。
触摸事件的响应
通过hitTest:withEvent:
已经找到了最佳响应者,现在要做的事情是让这个最佳响应者触摸事件。这个最佳响应者对于触摸事件拥有决定权,它可以决定是自己响应这个事件,也可以自己响应之后还把它传递给其他响应者,这个响应者构成的就是响应链。
响应者对于事件的响应和传递都是在touchesBegan:withEvent
这个方法中完成的。该方法默认的实现是将该方法沿着响应链往下传递
响应者对于接受到的事件有三种操作:
- 1.默认的操作。不拦截,事件会沿着默认的响应链自动往下传递。
- 2.拦截,不在往下分发事件,重写
touchesBegan:withEvent:
方法,不调用父类的touchesBegan:withEvent:
方法。 - 3.不拦截,继续往下分发事件,重新
touchesBegan:withEvent
方法,并调用父类touchesBegan:withEvent:
方法。
我们一般在编写代码时,如果某个视图响应事件,会在该视图类中重写touchesBegan:withEvent:
方法,但是并不会调用父类的touchesBegan:withEvent:
方法,这样我们就把这个事件拦截下来,不在沿着响应链往下传递。那么我们为什么想要沿着响应链传递事件就要写父类的touchesBegan:withEvent:
方法呢?因为父类的touchesBegan:withEvent:
方法默认是向下传递的。重写touchesBegan:withEvent:
并调用父类的方法就是既对触摸事件实现了响应,又将事件沿着响应链传递了。
响应链中的事件传递规则
每一个响应者对象都又一个nextResponder
方法,用来获取响应链中当前响应者对象的下一个响应者。硬刺,如果事件的最佳响应者确定了,那么整个响应链也就确定了。
对于响应者对象,默认的nextResponse
对象如下:
- UIView
若视图是UIViewController的view,则其nextResponder
是UIViewController,若其只是单独的视图,则其nextResponser
是其父视图。 - UIViewController
若该视图是window的根视图,则其nextResponser
是UIViewController,若其是由其他视图控制器present的,则其nextResponder是presenting view controller。 - UIWindow
nextResponder为UIApplication对象。
如果最佳响应者对象是UITextField,则响应链为:UITextField->UIView->UIView->UIViewController->UIWindow->UIApplication->UIApplicationDelegate.在父类的touchesBegan:withEvent:
方法中,可能调用了[self.nextResponder touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event]
这样来将事件沿着响应链传递。
UIControl
UIControl
是系统提供的能够以target-action
模式处理触摸事件的控件,iOS中UIButton
、UISegmentedControl
、UISwitch
等控件都是UIControl的子类。当UIControl
跟踪到触摸事件时,会向其上添加的target
发送事件以执行action
。UIConotro
l是UIView
的子类,因此本身也具备UIResponder
应有的身份。