前言:
事件 是界面交互(或人机交互)的最基本组成之一。没有它手机上的App就失去了存在的意义。
一个事件的周期:事件的产生——事件的传递——事件的响应
事件传递到响应实现原理:hitTest:withEvent: 和pointInside:withEvent 和touches方法
事件传递和事件响应区别:
事件的传递是自上而下(父控件到子控件);事件的响应是自下而上(顺着响应者链条向上传递:子控件到父控件。
iOS中的事件类型
- 触摸事件(touch events)
- 按压事件(press events)
- 摇晃事件(shake - motions events)
- 远程控制事件 (remote - controls events)
- 编辑菜单消息事件(editing menu messages)
一、事件的产生和传递
- 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中为什么是队列而不是栈?因为队列的特点是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
- UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
- 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
1.1 事件传递原理
事件传递是通过下面两个方法完成的:
-
hitTest:withEvent: (UIView 的实例方法)
-
pointInside:withEvent (UIView 的事例方法)
传递过程:
- 主窗口接收到应用传过来的事件后,首先判断能不能接收事件。如果能,就会判> 断触摸点在不在自身范围内。(通过pointInside:withEvent判断)如果不在,就不处理。如果在就进行第二步。
- 如果触摸点在自己的坐标范围内,那么窗口会从后往前遍历自己的子控件,来寻找最合适的view。(通过hitTest:withEvent:方法递归寻找)
- 遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
- 按照上面步骤循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就是最合适的view。
UIView不能接收事件的三种情况:
1. 不允许交互 `userInteractionEnabled` = NO
2. 透明度 `alpha` < 0.01
3. 父视图或者子视图的 `hidden` = YES
1.2 hitTest:withEvent:代码具体实现
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// event:触摸事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 判断自己能否接收事件
if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) {
return nil;
}
// 判断点是不是在当前视图上
if (![self pointInside:point withEvent:event]) {
return nil;
}
// 从后往前遍历自己的子控件,寻找更合适的View
for (long i = self.subviews.count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[i];
// 将自己坐标系的点转化成子控件坐标系的点
CGPoint childPoint = [self convertPoint:point toView:childView];
// 递归调用hitTest方法,寻找更加合适的View
UIView *fitView = [childView hitTest:childPoint withEvent:event];
if (fitView) {
return fitView;
}
}
// 没有找到比自己更适合的View
return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return NO;
}
1.3 重写 hitTest:withEvent: 方法实用之处
1.3.1 拦截事件的处理
- 正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
- 不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
- 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
栗子:
//BView是一个view的子视图
//BView是UIButton的父视图,但是BView的userInteractionEnable= NO
//通过hitTest可以拦截事件的功能 就可以指定UIButton作为响应者。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *v in self.subviews) {
if ([v isKindOfClass:NSClassFromString(@"BView")]) {
for (UIView *vv in v.subviews) {
if ([vv isKindOfClass:NSClassFromString(@"UIButton")]) {
CGPoint insideP = [self convertPoint:point toView:vv];
BOOL isInsidePoint = [vv pointInside:insideP withEvent:event];
if (isInsidePoint) {
return vv;
break;
}
}
}
break;
}
}
return self;
}
1.3.2 return nil作用
return nil的含义:
hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。
特殊用法
谁都不能处理事件,窗口也不能处理。
重写window的hitTest:withEvent:方法return nil
只能有窗口处理事件。
控制器的view的hitTest:withEvent:方法return nil或者window的hitTest:withEvent:方法return self
二、事件的响应
2.1响应者对象(UIResponder)
iOS中,我们最常接触的事件是触摸事件,什么样的对象才能处理触摸事件呢?
只有继承UIResponder的类或者子类才可以响应和处理事件。因为UIResponder:是用于响应和处理事件的抽象对象。
具体可以处理触摸事件的类如下:
- UIApplication
- UIViewController (包括它的子类)
- UIView (包括它的子类)
2.2 下面是UIResponder类提供的响应和处理触摸事件的4方法:
//开始接触屏幕,就会调用一次
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
//手指开始移动就会调用(这个方法会频繁的调用,其实一接触屏幕就会多次调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
//手指离开屏幕时,调用一次
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
//触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者view上面添加手势时,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
通过这4个方法就可以知道触摸事件的整个过程。四个方法中的两个参数UITouch 和 UIEvent 解释下:
UITouch对象
- 当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象
- 一根手指对应一个UITouch对象
UITouch作用
- 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
- 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触 摸位置
- 当手指离开屏幕时,系统会销毁相应的UITouch对象
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;
//获取手指与屏幕的接触半径 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,copy) NSArray *gestureRecognizers NS_AVAILABLE_IOS(3_2);
//获取触摸压力值,一般的压力感应值为1.0 IOS9 只读
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
//获取最大触摸压力值
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);
//取得在指定视图的位置
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0,0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(nullable UIView *)view;
//该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;
UIEvent 对象
每产生一个事件,就会产生一个UIEvent对象
UIEvent:称为事件对象,记录事件产生的时刻和类型
主要属性和方法
//事件类型
@property(nonatomic,readonly) UIEventType type;
//事件子类型
@property(nonatomic,readonly) UIEventSubtype subtype;
//事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
//返回值:返回与接收器相关联的所有触摸对象。
- (nullable NSSet *)allTouches;
// 返回值:返回属于一个给定视图的触摸对象,用于表示由接收器所表示的事件。
- (nullable NSSet *)touchesForView:(UIView *)view;
//返回值:返回属于一个给定窗口的接收器的事件响应的触摸对象。
- (nullable NSSet *)touchesForWindow:(UIWindow *)window;
//返回值:返回触摸对象被传递到特殊手势识别
- (nullable NSSet *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture
终于说完了UITouch 和 UIEvent ,其实还有很多细节,这里就不展开说了,回到touches的四个方法上来说事情:
- touches 里包含了一个或多UITouch对象,也即一个或多个手指同时触摸view,因此touches.count就是触摸的点数,是1就是单点触摸,大于1就是多点触摸。
- 一次触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数
如果两根手指同时触摸一个view,那就是一个事件,view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象- 如果这两根手指一前一后分开触摸同一个view,那就是两个事件,view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
响应者链传递过程示意图:
以上四个方法就是view用来响应事件然后做出处理的过程。
注意事项:
- 在view上使用这四个方法,必须自定义view然后重写父类方法。
- 在viewController里,重写这四个方法,是响应viewController的事件,viewController上的子视图需要响应和处理事件需要自定义,不要搞混淆了,以为在viewController里面写,就是重写了view的这四个方法。
- 如果当前这个view是控制器的view,那么控制器就是上一个响应者
- 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
view重写touches方法,处理事件的缺陷
1. 必须自定义View
2. 由于是View内部的touches方法中监听触摸事件,因此默认情况下无法让其他外界对象监听View的触摸事件
3. 不容易区分用户的具体手势行为。
使用技巧:
如何做到一个事件多个对象处理?
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
关于iOS-UIGestureRecognizer请看 iOS-手势详解
本文借鉴了一些前辈的文章,如果有不对的地方请指正,欢迎大家一起交流学习。