文章目录
- iOS中的事件
- UIResponder(响应者对象)
- UIResponder常用API
- UITouch(触摸事件对象)
- UITouch的作用
- UITouch常见API
- iOS事件的产生和传递
- 事件的产生
- 事件的传递
- 寻找最合适的视图底层剖析
- 事件的响应
- 响应者链
- nextResponder
- 总结
iOS中的事件
iOS的事件分为3大类型:触摸事件、加速计事件、远程控制事件;而我们最常用到的是触摸事件。
UIResponder(响应者对象)
在iOS中不是任何对象都能处理事件,只有继承了UIResponder
的对象才能接受并处理事件,我们称之为“响应者对象”。UIApplication
、UIViewController
、UIView
都继承UIResponder
。
UIResponder常用API
事件的处理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
对象,一根手指对应一个UITouch
对象; - 如果两根手指同时触摸一个视图,那么视图只会调用一次的
touchesBegan:withEvent:
方法方法,触摸参数中装着2个UITouch
对象; - 如果这两根手指一前一后分开触摸同一个视图,那么视图会分别调用2次的
touchesBegan:withEvent:
方法方法,并且每次调用时的触摸参数中只包含一个UITouch
对象。
UITouch的作用
- 保存着跟手指相关的信息,比如触摸的位置,时间,阶段;
- 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置;
- 当手指离开屏幕时,系统会销毁相应的UITouch对象。
UITouch常见API
//常见属性
//触摸产生时所处的窗口
@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创建是没有意义的。
iOS事件的产生和传递
事件的产生
- 发生触摸事件后,系统会将该事件加入到一个由
UIApplication
的管理的事件队列中(FIFO,先进先出),先产生的事件先处理。 - 的UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口
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<UITouch *> *)touches withEvent:(UIEvent *)even
方法;
// 此方法返回的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:
方法。
- 综上事件的传递顺序是这样的:
触摸事件 - > UIApplication事件队列 - > [UIWindow hitTest:withEvent:] - >返回更合适的视图 - > [子控件hitTest:withEvent:] - >返回最合适的视图
事件的响应
当事件产生并传递找到最合适的控件,就会调该用控件的触摸方法来作具体的事件处理(也就是响应该事件),如果该控件没有响应触摸事件(有没有重写touchesBegan
、touchesMoved
、touchesEnded
、touchesCancelled
这些响应触摸事件的方法),这些触摸方法的默认做法是将事件顺着响应者链条向上传递(也就是触摸方法默认不处理事件,只传递事件)。
响应者链
- 响应者链条示意图
响应者链是由多个响应者对象连接起来的链条。 - 响应者链的事件响应传递过程:
- 如果当前视图是控制器的视图,那么控制器就是下一个响应者,如果当前视图不是控制器的视图,那么父视图就是当前视图的下一个响应者;
- 如果当前响应者不处理事件,就将事件传递给下一个响应者,以此类推;
- 在视图层次结构的底顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给窗口对象进行处理;
- 如果窗口对象也不处理,则其将事件或消息传递给
UIApplication
的对象; - 如果
UIApplication
的也不能处理该事件或消息,则将其丢弃。
- 事件响应的底层原理:
只要点击控件,就会调用touchBegin
等那4个触摸事件响应方法,如果没有重写这个方法,自己处理不了触摸事件,系统默认就会用super
的touchesBegan
方法,直到有响应者重写过改法后,不再传递该触摸事件了。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event];
}
nextResponder
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。