参考文件
- UIResponder
- iOS编程中的快递小哥-Responder Chain(响应链)
- 史上最详细的iOS之事件的传递和响应机制-原理篇
1.事件类别
- Touch events
UIView
上的常见点击事件 - Press events
AppleTV
遥控器或者游戏控制器或其他带有实体物理键所触发的事件 - Shake-motion events
由加速计、陀螺仪、磁力仪触发的事件 - Remote-control events
额外配件如耳机上的音视频播放按键所触发的事件(视频播放、下一首)
1.1.响应者对象(UIResponder)
学习触摸事件首先要了解一个比较重要的概念-响应者对象(UIResponder)。
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
- UIApplication
- UIViewController
- UIView
那么为什么继承自UIResponder的类就能够接收并处理事件呢?
因为UIResponder中提供了以下4个对象方法来处理触摸事件。
UIResponder内部提供了以下方法来处理事件触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
2.响应链工作原理
从你手指触到到屏幕中某一控件到其响应相关事件其实是分为两步:事件的传递与事件的响应
事件的传递涉及到了UIView中的两个方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
//判断当前点击事件是否存在最优响应者(First Responder)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
//判断当前点击是否在控件的Bounds之内
事件的传递其实就是在事件产生与分发之后如何寻找最优响应视图的一个过程
2.1事件的传递流程
1.触碰屏幕产生事件UIEvent
并存入UIApplication
中的事件队列中, 并且在整个视图结构中自上而下的进行分发
2.UIWindow
接受到事件开始进行最优响应视图查询的过程(逆序遍历subviews
)
3.当到UIViewController
这一层时同样对其根视图(self.view及
其上subviews
)开始最优响应视图查询。该查询会调用上述提及到两个于UIView
的方法,之所以采用逆序查询也是为了优化查找速度,毕竟后addSubview
的视图在上易于命中
- 注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
UIView不能接收触摸事件的三种情况:
- 不允许交互:
userInteractionEnabled = NO
- 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
- 透明度:如果设置一个控件的
透明度<0.01
,会直接影响子控件的透明度。alpha:0.0~0.01
为透明。
注 意:默认UIImageView
不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO
。所以如果希望UIImageView
可以交互,需要设置UIImageView
的userInteractionEnabled = YES
。
/*
- 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
- 作用:寻找并返回最合适的view
- UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
- point:当前手指触摸的点
- point:是方法调用者坐标系上的点
*/
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01){
NSLog(@"%s--1.窗口 <不能> 接收事件",__func__);
return nil;
}
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) {
NSLog(@"%s--2.点 <不在> 窗口上",__func__);
return nil;
}
// 3.从后往前遍历子控件数组
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) {
NSLog(@"%s--3.从后往前遍历子控件数组 -- 遍历到了合适View",__func__);
// 如果能找到最合适的view
return fitView;
}
}
NSLog(@"%s--4.没有找到更合适的view,也就是没有比自己更合适的view",__func__);
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
/*
作者:VV木公子
鏈接:http://www.jianshu.com/p/2e074db792ba
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。
*/
测试事件传递过程 Demo
-
点击A
-
点击C
2.2事件的响应流程
响应链 其实是由一个个UIResponder的子类构成的,UIResponder是系统一个负责接受和处理事件的类。
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
而以上这几个响应触碰的方法其实也是出自于UIResponder
类,UIView
作为UIResponder
的子类能够处理点击事件也就无可厚非了。
具体事件的响应流程:
- 1.首先已确定最优响应视图
- 2.判断最优响应视图能否响应事件,如果视图能进行响应则事件在响应链中的传递终止。如果视图不能响应则将事件传递给
nextResponder
也就是通常的superview进行事件响应 - 3.如果事件继续上报至
UIWindow
并且无法响应,它将会把事件继续上报给UIApplication
- 4.如果事件继续上报至
UIApplication
并且也无法响应,它将会将事件上报给其Delegate
,但前提下这个Delegate
不属于 响应链 并且是UIResponder
的子类 - 5.如果最终事件依旧未被响应则会被系统抛弃
Note:
也并非所有的nextResponder
即是superview
,比如UIViewController
的根视图self.view
的nextResponder
是其所在UIViewController
。而如果UIViewController
如果是UIWindow
的根控制器,那么它的nextResponder
就是UIWindow
,但如果UIViewController
是另外一个UIViewController
present
出来的话,那么它的nextResponder
就是之前所执行present操作的那个UIViewController
测试事件响应流程 Demo
- 点击『绿色』