一、概述:
ios事件分发机制即寻找当前 交互事件(UIEvent) 的最佳响应的View并回调该View的响应方法。
流程总体可抽象为画“V”字型,起点为UIApplication,底部顶点为最佳响应View,终点为消费事件的View
图解:
V字通常只有左侧(探查过程)+ 顶点(最佳响应View并回调touches等响应方法)而不会画完
有右侧通常是最佳响应View没有处理或进一步调用父View的touches响应方法
若均未消费则回到UIApplication才画完完整的“V”字
\ · ———————— UIApplication
\ ·
\ ·
\ ·
\ ·
· -------- 最佳响应View,回调touches方法
- 事件传递(“\”过程):
hitTest:withEvent:
方法(内含pointInside:withEvent:
方法定位点击处View)寻找最佳响应的View(即“V”的顶点) - 事件响应(“/”过程):响应者链,
touches
系列方法响应
二、定义
https://www.cnblogs.com/wujy/p/5820825.html
1、事件分发的对象:UIEvent
- UIEvent源码:
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIEvent : NSObject
//事件类型
@property(nonatomic,readonly) UIEventType type API_AVAILABLE(ios(3.0));
@property(nonatomic,readonly) UIEventSubtype subtype API_AVAILABLE(ios(3.0));
//时间戳
@property(nonatomic,readonly) NSTimeInterval timestamp;
//所有的触摸
- (nullable NSSet <UITouch *> *)allTouches;
//获得UIWindow的触摸
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
//获得UIView的触摸
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
//获得事件中特定手势的触摸
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);
//会将丢失的触摸放到一个新的 UITouch 数组中
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
//辅助UITouch的触摸,预测发生了一系列主要的触摸事件(可能不完全匹配的触摸的真正的行为)
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
@end
- UIEventType及UIEventSubType源码:
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches, // 触摸事件(通过触摸、手势进行触发,例如手指点击、缩放)
UIEventTypeMotion, // 运动事件(通过加速器进行触发,例如手机晃动)
UIEventTypeRemoteControl, // 远程控制事件(通过其他远程设备触发,例如耳机控制按钮)
UIEventTypePresses API_AVAILABLE(ios(9.0)),
UIEventTypeScroll API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 10,
UIEventTypeHover API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 11,
UIEventTypeTransform API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 14,
};
typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,
// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,
// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};
2、(触摸)事件序列:UITouches
UITouches是触摸事件(UIEventType = UIEventTypeTouches
)的事件序列
- 通常事件周期为:down * 1 -> move * n -> up * 1
//触摸事件周期
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, //开始触摸 (down)
UITouchPhaseMoved, //移动 (move)
UITouchPhaseStationary, //停留
UITouchPhaseEnded, //触摸结束 (up)
UITouchPhaseCancelled, //触摸中断
};
//检测是否支持3DTouch
typedef NS_ENUM(NSInteger, UIForceTouchCapability) {
UIForceTouchCapabilityUnknown = 0, //3D Touch检测失败
UIForceTouchCapabilityUnavailable = 1, //3D Touch不可用
UIForceTouchCapabilityAvailable = 2 //3D Touch可用
};
NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject
//触摸产生或变化的时间戳 只读
@property(nonatomic,readonly) NSTimeInterval timestamp;
//触摸周期内的各个状态
@property(nonatomic,readonly) UITouchPhase phase;
//短时间内点击的次数 只读
@property(nonatomic,readonly) NSUInteger tapCount;
//获取手指与屏幕的接触半径 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
//获取手指与屏幕的接触半径的误差 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
//触摸时所在的窗口 只读
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
//触摸时所在视图
@property(nullable,nonatomic,readonly,strong) UIView *view;
//获取触摸手势
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);
//取得在指定视图的位置
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0,0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(nullable UIView *)view;
//该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;
//获取触摸压力值,一般的压力感应值为1.0 IOS9 只读
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
//获取最大触摸压力值
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);
@end
三、过程
1、事件传递(“\”过程)
- 目的:寻找最佳响应View
- 关键方法:
hitTest:withEvent:
(寻找最佳响应View) 和pointInside:withEvent:
(判断触摸点是否在该View内部)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1、判断是否可以接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2、判断点是否在当前视图上
//(tips:所以如果触摸的点不在父view上,那么其上的所有子view的hitTest都不会被调用,即使clipsToBounds = NO)
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3、循环遍历所有子视图,查找是否有更合适的子视图(注意:是从后向前遍历subViews)
for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
UIView *childView = self.subviews[i];
//转换点到子视图坐标系上
CGPoint childPoint = [self convertPoint:point toView:childView];
//递归查找是否存在最合适的view
UIView *fitView = [childView hitTest:childPoint withEvent:event];
//如果返回非空,说明子视图中找到了最合适的view,那么返回它
if (fitView) {
return fitView;
}
}
//循环结束,仍旧没有合适的子视图可以处理事件,那么就认为自己是最合适的view
return self;
}
- 流程图:
2、事件响应(“/”过程)
在找到最合适的view之后,会调用view的 touchesXXX:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
(触摸事件)等响应方法对事件进行响应
- 默认不处理,沿 响应者链(Responser Chain) 向上传递给下一个响应者
- 如果下一个响应者的touches等响应方法没有重写,事件会继续沿着响应者链往上走
- 如果传递到UIApplication,依旧不能处理事件那么事件就被丢弃
响应链节点 | 表现 |
---|---|
UIView | 如果view是viewcontroller的根view,那么下一个响应者是viewcontroller,否则是super view |
UIViewcontroller | 如果viewcontroller的view是window的根view,那么下一个响应者是window;如果viewcontroller是另一个viewcontroller模态推出的,那么下一个响应者是另一个viewcontroller;如果viewcontroller的view被add到另一个viewcontroller的根view上,那么下一个响应者是另一个viewcontroller的根view |
UIWindow | UIWindow的下一个响应者是UIApplication |
UIApplication | 通常UIApplication是响应者链的顶端(如果app delegate也继承了UIResponder,事件还会继续传给app delegate) |
- 响应者链构成:由一系列可以响应事件的对象(继承于
UIResponder
)组成 - 响应者链的头节点:最佳响应View
- 响应者链作用:决定了响应者对象响应事件的先后顺序,
- 响应者链生成时机:在递归查找最合适的view的时候形成
四、实践场景
部分实例可参考:https://www.jianshu.com/p/74a2f44840fa
1、事件拦截
- 场景:父View不想让子View感知到某些类型的事件,或父View指定响应的子View
- 方法:重写 父View 的
hitTest
方法,不通过正常 遍历 寻找最佳子View,而是返回自身或指定子View - 原理:原最佳响应的View未被父View通过正常的 递归
hitTest
遍历到,而是直接被父View返回了 指定View(即“V”的顶点) 作为最佳响应View
图解:
\ ·
\ ·
\ ·
· --------父View,指定自身为最佳响应View
· ------- 子View,无法感知事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
if ([self pointInside:point withEvent:event] == NO) return nil;
//return self; // 情况1:返回自身,事件传递到此为止,自己就是最佳响应View
return self.subviews[0]; // 情况2:返回指定子View,指定的View即为最佳响应的View
}
2、事件透传
- 场景:当前View不响应事件,由父View响应
- 方法:重写 当前View 的
hitTest
方法,retun nil - 原理:子View(原“V”的顶点,即原最佳响应View) 的hitTest返回了nil,则父View 的hitTest遍历子View未找到最佳响应的View,则return self使 自己(即父View)成为 新的最佳响应的View(新的“V”的顶点),从而会 自动调用 父View的
touches
方法响应
图解:
\ · \ ·
\ · \ ·
\ · ---> \ ·
\ · · --------父View,新的最佳响应View,指定自身为最佳响应View
· · ------- 子View,原最佳响应View,可感知事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 可以感知到事件,但不处理,进入“/”(事件响应)流程
return nil;
}
3、事件多层级响应
- 场景:当前View响应事件、父View也可响应事件
- 方法:重写 当前View 的
touchesXXX:withEvent:
响应链方法,自己的事件响应完后调用[super touchesXXX:touches withEvent:event];
- 原理:当前View已经被确认为最佳响应View,并被回调touches方法响应,但 主动调用 父View的touches方法
图解:
\ · \ ·
\ · \ ·
\ · ---> \ ·
\ · \ / --------父View,响应回调,被子View主动调用touches事件
· · ------- 子View,响应回调,因是最佳响应View,所以“自动”调用touches事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"red touches begin"); //自己的处理
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"red touches moved"); //自己的处理
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"red touches end"); //自己的处理
[super touchesBegan:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"red touches canceled"); //自己的处理
[super touchesBegan:touches withEvent:event];
}