UIControl、UIEvent、UITouch、UIPress、UIResponder
在讨论事件、响应链和传递链之前,我们先来认识一下相关的几个类。
UIControl
该类下的核心方法大家应该都见过:
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
所以所有能调用这个方法的控件,比如UIButton、UITextField等等,都是继承自这个类。
UIControl的主要功能:为一些交互类控件添加action。它控制一个控件在某个状态下(比如按下、抬起等)要发生什么事件。
UIEvent
是由硬件捕获到的一个表示用户操作设备的对象,事件分为三类:包括触摸事件(Touch Events对应就是UITouch)、运动事件(Motion Events)、远程控制事件(Remote Control Events)。
UITouch
紧接着UIEvent,UITouch这个类就详细地表明的手指触摸屏幕的这个动作的具体参数,比如触摸类型(直接一根手指、还是用触摸笔)、触摸状态(一根手指开始接触屏幕、在屏幕上移动等等)、触摸力度、触摸事件、几个手指触摸等等。
总结一下,这个类是用来定义实际触摸动作的参数。
@interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp; // 触摸发起的时间
@property(nonatomic,readonly) UITouchPhase phase; // 触摸的各个阶段状态
@property(nonatomic,readonly) NSUInteger tapCount; // 快速双击
@property(nonatomic,readonly) UITouchType type API_AVAILABLE(ios(9.0)); //手指 or pencil
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
@property(nullable,nonatomic,readonly,strong) UIView *view;
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers
...
@end
// 触摸的各个阶段状态,
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
UITouchPhaseRegionEntered API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos), // whenever a touch is entering the region of a user interface
UITouchPhaseRegionMoved API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos), // when a touch is inside the region of a user interface, but hasn’t yet made contact or left the region
UITouchPhaseRegionExited API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos), // when a touch is exiting the region of a user interface
};
UIPress
UIPress跟UITouch类似:UITouch表示触摸的相关信息,UIPress则表示按压的相关信息。
UIResponder
UIResponder是响应对象的基类,定义了处理各种事件的接口,专门用来响应用户的操作和处理各种事件。常见的子类有:UIView,UIViewController,UIApplication和UIApplicationDelegate。
UIResponder提供了用户点击、按压检测以及手势检测的回调方法,分别对应用户开始、移动、结束以及取消,其中只有在程序强制退出或者来电时,取消事件才会调用。
UIResponder协议函数
- (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)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 //用户按压行为被打断触发,IOS9以上支持
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event //陀螺仪或加速设备开始发生改变触发
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event //陀螺仪或加速设备发生改变过程触发
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event //陀螺仪或加速设备改变过程被其他行为中断异常触发
UIViewController利用UIResponder实例
// UIViewController.m
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"屏幕被手指按下了");
// 获取手指列表
NSArray<UITouch*>* toucheList = [touches allObjects];
for (int i = 0; i < toucheList.count; i ++) {
UITouch* touch = toucheList[i];
NSLog(@"获取手指15s内同一位置点击次数 -> 手指 %d 15s内点击了 %zd 次", i, touch.tapCount);
}
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"手指移动啦");
// 获取手指列表
UITouch* touch = [touches allObjects][0];
// 获取手指相对于指定视图的x,y轴
CGPoint position = [touch locationInView:self.view];
NSLog(@"手指坐标为x = %f,y = %f", position.x, position.y);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"手指离开了");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"触摸过程被其他行为异常中断");
}
事件
iOS中的事件分成好几种:
- 触摸事件,手指触摸屏幕时产生
- 加速事件,手机的陀螺仪以及加速度计产生的
- 远程控制事件,使用其他远程控制设备控制,例如蓝牙设备
下面,我们讨论的都是触摸事件。
事件流程
整个事件传递和处理流程,简单概括为:
事件——事件传递到指定界面——找到可响应的界面——响应
- 当有用户触摸屏幕的时候产生事件,系统硬件进程获取到这个事件,并处理封装保存在系统中,由于系统硬件进程和app进程是两个不同的进程,所以使用进程间的端口通信。
- 系统会将这个事件加入到UIApplication的事件管理队列中,事件从队列中出队后通常会发送给app的keywindow处理。
- keywindow会找到一个最适合的视图去处理事件。也就是从super控件到子控件中。
简单总结事件流程:UIApplication->window->寻找处理事件最合适的view
下面我们详细讨论这个过程。
事件链
以上图为例:
点击屏幕时,首先UIApplication对象先收到该点击事件,再依次传递给它上面的所有子view,直到传递到最上层。
我们有传递链:UIApplication——>UIWindow——>RootViewController——>View——>Button
,即绿色箭头。
反之,也有响应链Button——>View——>RootViewController——>UIWindow——>UIApplication
,即红色箭头。
简单总结,事件链包含传递链和响应链,一般事件先通过传递链传递下去。如果上层不能响应,那么一层一层通过响应链找到能响应的UIResponse。
传递链
传递链由系统向最上层view传递:Application -> window -> root view -> ... -> first view
具体过程
- 当触摸事件发生时,压力传为电信号,iOS系统将产生UIEvent对象,记录事件产生的时间和类型。
- 当检测到一个系统事件、例如屏幕上的点击,UIKit内部创建一个UIEvent实例并且记录事件产生的时间和类型,然后系统将事件加入到一个由UIApplication管理的事件队列中。
- UIApplication会从消息队列中取出该事件传递给UIWindow对象,在UIWindow中调用方法
hitTest:withEvent:
返回最终相应的view。 - 在
hitTest:withEvent:
方法中调用pointInside:withEvent:
来判断当前点击的点是否在UIWindow内部,如若返回yes,则倒序遍历其子视图找到最终响应的子view。 - 如果最终返回一个view,那么即为最终响应view并结束事件传递,如果无值返回则将UIWindow作为响应者。
过程中提到的两个核心方法:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
hitTest:withEvent:
该方法的处理流程:
- 首先调用当前视图的pointInside:withEvent: 方法判断触摸点是否在当前视图内。
- 若返回NO,则hitTest:withEvent: 返回 nil。
- 若返回YES,则向当前视图的所有子视图发送
hitTest:withEvent:
消息,所有子视图的遍历顺序是从最顶视图一直到最低层视图,即从 subviews 数组的末尾向前遍历,直到有子视图返回非空对象,或者全部子视图遍历完毕。 - 若第一次有子视图返回非空对象,则
hitTest:withEvent:
返回此对象,处理结束。 - 若所有子视图都返回空,则
hitTest:withEvent:
返回自身。
具体处理案例:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *touchView = self;
if ([self pointInside:point withEvent:event] &&
(!self.hidden) &&
self.userInteractionEnabled &&
(self.alpha >= 0.01f)) {
for (UIView *subView in self.subviews) {
[subview convertPoint:point fromView:self];
UIView *subTouchView = [subView hitTest:subPoint withEvent:event];
if (subTouchView) {
touchView = subTouchView;
break;
}
}
} else {
touchView = nil;
}
return touchView;
}
该方法不接受事件处理的情况
- hidden = YES,隐藏的视图
- userInteractionEnabled = NO,禁止用户操作的视图
- alpha <0.01, 透明视图
注意
- 如果最终hitTest 没有找到第一响应者,或第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯。若最终 UIWindow UIApplication都不能处理该事件,则会被丢弃。
- 如果一个子视图的区域超过父视图的 bound 区域(父视图的 clipsToBounds 属性为 NO,这样超过父视图 bound 区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别, 因为父视图的
pointInside:withEvent:
方法会返回 NO, 这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:
方法来处理这种情况。 - 我们可以重写
hitTest:withEvent:
来达到某些特定的目的。
响应链
在苹果的官方文档中,有使用响应者和响应者链来处理事件。
响应链由最基础的view向系统传递:
first view -> super view -> ... -> view controller -> window -> Application -> AppDelegate
响应链是 UIKit 生成的 UIResponder 对象组成的链表,它同时还是 iOS 里一切相关事件(例如触摸和动效)的基础。
当iOS捕获到某个事件时,就会将此事件传递给某个看上去最适合处理该事件的对象,比如触摸事件传递给手指刚刚触摸位置的那个视图(view),如果这个对象无法处理该事件,iOS系统就继续将该事件传递给更深层的对象,直到找到能够对该事件作出响应处理的对象为止。这一连串的对象序列被称作为“响应链”(responder chain),iOS系统就是沿着此响应链,由最外层逐步向内层对象传递该事件,亦即将 “处理该事件的责任” 进行传递。iOS的这种机制,使得事件处理具有协调性和动态性。
具体流程:
- 初始视图先进行尝试处理,自身处理不了的情况下,如果view的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的父视图
- 在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给UIWindow对象进行处理
- 如果UIWindow对象也不处理,则将事件传递给UIApplication对象
- 如果UIApplication也不能处理该事件,则将该事件丢弃