前言,本文简单了解触摸事件传递和事件响应者链。
一、知识点简介
1.1 iOS中的事件介绍
iOS中的事件可以分为3大类型:
- 触屏事件(例如点击按钮、通过手势缩放图片、拖动上下滚动页面等)
- 加速计事件(例如摇一摇红包、通过旋转设备控制赛车方向、指南针等)
- 远程控制事件(例如耳机的线控、外接手柄、遥控器等)
iOS处理触屏事件,分为两种方式:
- 高级事件处理:利用UIKit提供的各种用户控件或者手势识别器来处理事件。
- 低级事件处理:在UIView的子类中重写触屏回调方法,直接处理触屏事件。
1.2 UIGestureRecognizer
UIKit中我们常用的是UIControl类实例的addTarget:action:forControlEvents:
方法维护控件目标行为表,除了UIKit控件外,手势识别器UIGestureRecognizer类的实例也可以处理触屏事件,其内部也使用目标行为表。
UIKit内置了6种手势识别器:
UITapGestureRecognizer:点击(单击、双击、三连击等)手势。
UIPinchGestureRecognizer:缩放手势。
UIPanGestureRecognizer:拖拽手势。
UISwipeGestureRecognizer:滑动手势。
UIRotationGestureRecognizer:旋转手势。
UILongPressGestureRecognizer:长按手势。
1.3 UITouch
当你用一根手指触摸屏幕时, 会创建一个与之关联的UITouch对象, 一个UITouch对象对应一根手指. 在事件中可以根据NSSet中UITouch对象的数量得出此次触摸事件是单指触摸还是双指多指等等.
触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;
短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch对象
1.4 UIEvent
每产生一个事件, 就对应产生一个UIEvent. UIEvent记录着该事件产生的时间, 事件的类型等等.
UIEvent几个重要的属性 :
事件类型
@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
1.5 响应者对象(UIResponder)
在iOS中不是任何对象都能处理事件, 只有继承了UIResponder的对象才能接收并处理事件,我们称为响应者对象.
UIApplication,UIViewController,UIView都继承自UIResponder,因此他们都是响应者对象, 都能够接收并处理事件.
继承自UIResponder的类能处理事件是由于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;
两个UIView相关属性:
- multipleTouchEnabled:是否开启多点触控
- exclusiveTouch :多个控件接受事件时的排他性
二、事件传递
- 1.发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的队列事件中
- 2.UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)
- 3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件
注:一般事件的传递是从父控件传递到子控件的;如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件。
例如:
- 点击了绿色的View,传递过程如下:UIApplication->Window->白色View->绿色View
- 点击蓝色的View,传递过程如下:UIApplication->Window->白色View->橙色View->蓝色View
2.1 查找合适 View 步骤
应用查找到最合适的控件来处理事件,有以下准则
- 1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
- 2.触摸点是否在自己身上
- 3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
- 4.如果没有符合条件的子控件,那么就认为自己最合适处理
详细步骤:
- 1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
- 2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
- 3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
- 4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。
判断UIView不能接收触摸事件的三种情况:
- 不接受用户交互:userInteractionEnabled = NO;
- 隐藏:hidden = YES;
- 透明:alpha = 0.0~0.01
2.2 hitTest:withEvent:方法
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法寻找合适的View.
作用:
寻找并返回最合适的view(能够响应事件的那个最合适的view)
不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
判断当前View 是否满足接收触摸事件的条件(1.用户交互 2. 隐藏 3. 透明度)
底层具体实现如下 :
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判断当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2. 判断点在不在当前控件
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历自己的子控件
NSInteger count = self.subviews.count;
for (NSInteger 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) { // 寻找到最合适的view
return fitView;
}
}
// 循环结束,表示没有比自己更合适的view
return self;
}
如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的,判断所有子控件不合适最后返回父控件。
2.3 pointInside:withEvent:方法
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
该方法判断触摸点是否在控件身上, 是则返回YES, 否则返回NO
搭配hitTest:withEvent:
达到目的:
- 明明点击的是B视图, 却由A视图来响应事件
- 穿透某控件点击被覆盖的下一层控件
- 让父控件frame之外的子控件响应触摸事件
2.4 视图不响应
发现视图无法响应点击事件,可以检查下面几项:
1、hidden = YES 视图被隐藏
2、userInteractionEnabled = NO 不接受响应事件
3、alpha <= 0.01,透明视图不接收响应事件
4、子视图超出父视图范围
5、需响应视图被其他视图盖住
6、是否重写了其父视图以及自身的hitTest方法
7、是否重写了其父视图以及自身的pointInside方法
三、事件响应
找到合适的View之后就会调用该view的touches方法要进行响应处理具体的事件,找不到最合适的view,就不会调用touches方法进行事件处理。
响应者链条其实就是很多响应者对象(继承自UIResponder的对象)一起组合起来的链条称之为响应者链条。
一般默认做法是控件将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理 (即调用super的touches方法)。
判断当前响应者的上一个响应者是谁:
- 1.判断当前是否是控制器的View,如果是控制器的View,上一个响应者就是控制器
-
2.如果不是控制器的View,上一个响应者就是父控件
3.1 响应链
响应链是“事件派发”的原则和规定,那么响应链是什么?顾名思义事件链是一个链条,详细的定义如下:
- 每条链是一个 链表状结构,整个是一棵树
-
链表的每一个node是一个 UIResponser对象
3.2 touch 响应
- 找到最合适的view会调用touches方法处理事件
- touches默认做法是把事件顺着响应者链条向上抛
如果控制器不响应响应touches方法,就交给UIWindow。如果UIWindow也不响应,交给UIApplication,如果都不响应事件就作废了。
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event];
// 注意不是调用父控件的touches方法,而是调用父类的touches方法
// super是父类 superview是父控件
}
3.3 触摸事件传递响应过程:
完整的触摸事件的传递响应过程为:
UIApplication–>UIWindow–>递归找到最合适处理的控件–>控件调用touches方法–>判断是否实现touches方法–>没有实现默认会将事件传递给上一个响应者–>找到上一个响应者–>找不到方法作废
总结:触摸或者点击一个控件,然后这个事件会从上向下(从父->子)找最合适的view处理,找到这个view之后看他能不能处理,能就处理,不能就按照事件响应链向上(从子->父)传递给父控件
事件的传递和响应的区别:
- 事件的传递是从上到下(父控件到子控件)
- 事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。