iOS的事件分为3大类型:触摸事件、加速计事件、远程控制事件;而我们最常用到的是触摸事件。
在iOS中不是任何对象都能处理事件,只有继承了UIResponder
的对象才能接受并处理事件,我们称之为“响应者对象”。UIApplication
、UIViewController
、UIView
都继承UIResponder
。
事件的处理API
//UIResponder内部提供了以下方法来处理事件触摸事件
// 一根或者多根手指开始触摸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;
//加速计事件
- (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;
以上方法是由系统自动调用的,所以可以通过重写该方法来处理一些事件。
UITouch
对象,一根手指对应一个UITouch
对象;touchesBegan:withEvent:
方法方法,触摸参数中装着2个UITouch
对象;touchesBegan:withEvent:
方法方法,并且每次调用时的触摸参数中只包含一个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;
//常见方法
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(UIView *)view;
// 该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(UIView *)view;
注意 : UITouch
对象是当触摸时系统自动创建的,自己alloc创建是没有意义的。
UIApplication
的管理的事件队列中(FIFO,先进先出),先产生的事件先处理。keyWindow
。那么如何找到最合适的视图呢?这就看时间是如何传递的!
首先判断主窗口(keyWindow)自己是否能接受触摸事件;
判断触摸点是否在自己身上;
如果上面两部都满足,就将子控件数组中从后往前遍历子控件,让子控件重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1,2-步骤);
如果一个控件自己满足上面的条件,而它的所有子控件都不满足上面条件,或者其没有子控件,则该控件就是响应事件的最合适的视图。
UIView的不能接收触摸事件的三种情况:
alpha <0.01;
userInteractionEnabled = NO;
hidden = YES;
注意:采取从后往前遍历子控件的 方式寻找最合适的视图只是为了做一些循环优化。因为相比较之下,后添加的视图在上面,降低循环次数。
寻找响应事件的最合适的视图,需要用到两个关键的方法:
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
只要事件一传递给一个控件,这个控件就会调用他自己的则hitTest:withEvent
:方法方法,寻找并返回最合适的视图(能够响应事件的那个最合适的视图);
实例:
视图结构如下:GrayView
是view
的子视图,RedView、YellowView
是GrayView
的子视图,BlueView、GreenView
是RedView
的子视图,PurpleView,CyanView
是YellowView的
子视图;并且添加顺序是从上到下,从左到右。
在每个自定义的子视图中重新- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
和-(void)touchesBegan:(NSSet
方法;
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"事件传递到%@",NSStringFromClass([self class]));
// 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];
//子控件在重复调用自己的hitTest方法
UIView *fitView = [childView hitTest:childP withEvent:event];
// 如果子视图是最合适的就返回
if (fitView) {
return fitView;
}
}
// 循环结束,说明只有自己是最合适的view
return self;
}
//开始点击事调用,及响应事件处理
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%@是最佳的事件响应者",NSStringFromClass([self class]));
}
当点击GrayView
时打印结果:
当点击BlueView
时打印结果:
return nil的含义:
hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的视图不是合适的视图,子控件也不是合适的视图。如果同级的兄弟控件也没有合适的视图,那么最合适的视图就是父控件。
截事件的处理:
正因hitTest:withEvent:
方法方法可以返回最合适的视图,所以可以通过重写hitTest:withEvent:
方法方法,返回指定的视图作为最合适的图去响应事件。
想让谁成为最合适的视图就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法return self。但是,建议在父控件的则hitTest:withEvent:方法中返回子控件作为最合适的观点!
列如不管点击哪个视图都让YellowView
成为处理事件最合适的view,只需要将其俯视图(GrayView
)的hitTest:withEvent:
方法重写,返回YellowView
:
// 重写GrayView的hitTest:withEvent:方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"事件传递到%@",NSStringFromClass([self class]));
// 1.判断当前控件能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
for (UIView * childView in self.subviews) {
if ([childView isMemberOfClass:NSClassFromString(@"YellowView")]) {
return childView;
}
}
return self;
}
或者重写YellowView
的hitTest:withEvent:
返回self
;
//重写YellowView的该方法
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
注意: 因为YellowView
后添加到GrayView
,所以会优先将事件传递给它,这样可以实现点击任意视图YellowView
就是最合适的视图,但是如果想让RedView
成为最合适的视图,只重写RedView
的hitTest: withEvent:
方法并返回self
,当点击YellowView
或者YellowView
的子视图PurpleView
和CyanView
时返回的最合适视图并不是RedView
,所以要拦截某个视图为最合适的视图最好重写其俯视图的hitTest: withEvent:
方法。
当事件产生并传递找到最合适的控件,就会调该用控件的触摸方法来作具体的事件处理(也就是响应该事件),如果该控件没有响应触摸事件(有没有重写touchesBegan
、touchesMoved
、touchesEnded
、touchesCancelled
这些响应触摸事件的方法),这些触摸方法的默认做法是将事件顺着响应者链条向上传递(也就是触摸方法默认不处理事件,只传递事件)。
UIApplication
的对象;UIApplication
的也不能处理该事件或消息,则将其丢弃。touchBegin
等那4个触摸事件响应方法,如果没有重写这个方法,自己处理不了触摸事件,系统默认就会用super
的touchesBegan
方法,直到有响应者重写过改法后,不再传递该触摸事件了。- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event];
}
UIResponder
具有nextResponder
属性,也就是其SuperView
或是UIViewConterller
等,这个属性有时候还是很有用的,比如UIViewConterller
的View
的添加一个UITableView
,点击自定cell
跳转别的控制器,这时候就可以通过cell.superView.superView.nextResponder
就可以获取控制器,然后进行跳转。
从父控件传给子控件
,也就是说由UIApplication - > UIWindow - > UIView - >初始视图;从子控件传给父控件
去查找没有响应者去处理事件,如果都没有处理该事件,则该事件不被任何响应者响应,就抛弃该事件。UIView
时,系统会产生一个事件,并将其放入UIApplication
的事件队列中。然后该事件会顺着这条链传递到用户点击的那个UIView:UIApplication->UIWindow->RootView->...->Subview
。然后开始处理这个事件,若Subview
不处理,事件将会传递给视图控制器,若没有控制器则传给其superView
,最后传给UIWindow
,UIApplicatio
n。若UIApplication
还是没处理则将事件传给nil。