响应者链和事件传递
事件传递
事件是怎么产生和传递的?
- 发生触摸事件事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中。UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
- 先将事件对象由下往上传递(由父控件传给子控件)。
- 如果找到最合适的控件来处理调用最合适的控件的touches...(touchesBegan、touchesMoved、touchedEnded)方法。如果调用了[super touch…],就会将事件顺着响应者链往上传递,传给上一个响应者,接着上一个响应者就会调用touches...方法。
- 如果没有找到合适的控件来处理事件,则将事件传回来窗口,窗口不处理事件,将事件传给 UIApplication。如果 UIApplication 不能处理事件,则将其丢弃。
- 如何找到最合适的控件来处理事件?
- 自己是否能接收触摸事件?
UIView不接收触摸事件的三种情况不接收用户交互:
- userInteractionEnabled = NO隐藏
- hidden = YES;
- 透明:alpha = 0.0 ~ 0.01;
- 触摸点是否在自己身上? 判断触摸点在不在自己身上,view有一个方法pointInside,返回NO则不在自己身上,那就不再遍历子控件,返回YES,代表在自己身上,那就继续遍历子控件,从后往前遍历子控件,重复前面两个步骤如果没有符合条件的子控件,那么自己就是最适合处理的控件找到“最合适” 接收的控件后,调用控件touchesBegan,touchesMoved,touchedEnded的方法。
- 从后往前遍历子控件,重复前面的两个步骤
- 如果没有符合条件的子控件,那么就自己最适合处理
- 自己是否能接收触摸事件?
UIView不接收触摸事件的三种情况不接收用户交互:
事件传递示例
触摸事件的传递是从父控件传递到子控件,如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件。
- 点击了绿色的view:UIApplication -> UIWindow -> 白色 -> 绿色
- 点击了蓝色的view:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色
- 点击了黄色的view:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 -> 黄色
事件响应
概念
-
响应者对象
在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”。UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件。 -
什么是响应者? 继承了UIResponder的对象就是响应者。
-
什么是上一个响应者?
如果当前这个view是控制器的view,那么控制器就是上一个响应者;
如果当前这个view不是控制器的view,那么父控件就是上一个响应者。 -
响应者链条是什么
它是一种事件处理机制,由多个响应者对象连接起来的层次结构,使得事件可以沿着这些对象进行传递。利用响应者链条我们可以通过调用touches的super 方法,让多个响应者同时响应该事件。 -
响应者链条作用
利用响应者链条可以让多个控件处理同一个“触摸事件”; 在最后合适的控件调用super的touchBegan方法,这样就将事件传给上一个响应,上一个响应者也可以处理事件了。
响应者链的事件传递过程
如果view的控制器存在,就传递给控制器。 如果控制器不存在,则将其传递给它的父视图在视图层次结构的最顶级视图。 如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理。 如果window对象也不处理,则其将事件或消息传递给UIApplication对象。 如果UIApplication也不能处理该事件或消息,则将其丢弃。
响应者链条示意图
- 系统首先检查当前触摸到的视图是否响应事件,如果响应事件传递结束,否则转步骤2
- 系统检查当前触摸到的视图的控制器,如果控制器响应则事件传递结束;如果该视图没有控制器或者控制器不响应该事件,则转步骤3
- 系统检查父视图,再检查父视图的控制器...
- 最后,如果最顶层的视图/控制器也不响应则交给window
事件响应链 hitTest:withEvent: 伪代码
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的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) return nil;
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) 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) {
// 如果能找到最合适的view
return fitView;
}
}
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
复制代码
iOS中的各种事件
事件可以分为3大类型
- 触摸事件
- 加速计事件
- (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的属性
@property(nonatomic,readonly,retain) UIWindow *window;
触摸产生时所处的窗口@property(nonatomic,readonly,retain) UIView *view;
触摸产生时所处的视图@property(nonatomic,readonly) NSUInteger tapCount;
短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击@property(nonatomic,readonly) NSTimeInterval timestamp;
记录了触摸事件产生或变化时的时间,单位是秒@property(nonatomic,readonly) UITouchPhase phase;
当前触摸事件所处的状态
UITouch的方法
- (CGPoint)locationInView:(UIView *)view;
返回值表示触摸在view上的位置,这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0));调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置。- (CGPoint)previousLocationInView:(UIView *)view;
该方法记录了前一个触摸点的位置。
UIEvent
UIEvent:称为事件对象,记录事件产生的时刻和类型。 每产生一个事件,就会产生一个UIEvent对象。 UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)。 常见属性
@property(nonatomic,readonly) UIEventType type;
、@property(nonatomic,readonly) UIEventSubtype subtype;
事件类型@property(nonatomic,readonly) NSTimeInterval timestamp;
事件产生的时间
UITouch的作用
保存着跟手指相关的信息,比如触摸的位置、时间、阶段。 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。 当手指离开屏幕时,系统会销毁相应的UITouch对象。 提示:iPhone开发中,要避免使用双击事件!
触摸过程
一次完整的触摸过程,会经历3个状态:
- 触摸开始:
- (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
4个触摸事件处理方法中,都有NSSet *touches
和UIEvent *event
两个参数。
具体情况说明:
当用户用手指触摸屏幕时,会创建一个与手指相关联的UITouch对象。一根手指对应一个UITouch对象。
- 一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数。
- 如果两根手指同时触摸一个view,那么view只会调用一次
touchesBegan:withEvent:
方法,touches参数中装着2个UITouch对象。 - 如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次
touchesBegan:withEvent:
方法,并且每次调用时的touches参数中只包含一个UITouch对象。 - 根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸。
- 提示:touches中存放的都是UITouch对象
UITouch 代码举例
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
_imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
_imageView.image = [UIImage imageNamed:@"player2"];
[self.view addSubview:_imageView];
//userInteractionEnabled是从UIView继承来的一个属性,对很多视图来说其默认值都是YES,但是包括UIImageview在内的几个少数视图其默认值是NO
_imageView.userInteractionEnabled = YES;//允许与用户交互(才能响应触摸事件)
//1.一个视图要想与用户交互,除了自己的userInteractionEnabled为YES,其父视图的userInteractionEnabled也得为YES
//2.一个视图如果坐标超出了父视图也不能与用户进行交互
UIButton *btn = [UIButton buttonWithType:UIButtonTypeInfoLight];
btn.frame = CGRectMake(20, 20, 50, 50);
[_imageView addSubview:btn];//例子
[btn addTarget:self action:@selector(onBtnClick) forControlEvents:UIControlEventTouchUpInside];
//btn.userInteractionEnabled = NO;
}
-(void)onBtnClick {
NSLog(@"按钮被戳了");
}
// 触摸开始,任何一个视图或视图控制器都有的方法,当触摸的时候系统自动调用
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"触摸开始");
for (UITouch *touch in touches) {
CGPoint p = [touch locationInView:self.view];
_imageView.center = p;
}
}
// 触摸移动,按下手指并移动时系统自动调用
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"手指动了");
for (UITouch *touch in touches) {
//touch.view表示当前触控到的视图
NSLog(@"touch.view:%@",touch.view);
if (touch.view == _imageView) {
NSLog(@"摸到小人了");
//[touch locationInView: ]用于获取当前触摸的点相对于某个视图的坐标
CGPoint p = [touch locationInView:self.view];
_imageView.center = p;
}
}
}
// 触摸移动,按下手指并离开时系统自动调用
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"触摸停止");
}
// 由于某种原因(比如系统内存严重不足)触摸事件被系统强行取消时调用
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}
复制代码
UIGestureRecognizer(手势识别器)
利用UIGestureRecognizer,能轻松识别用户在某个view上面做的一些常见手势。
UIGestureRecognizer是一个抽象类,定义了所有手势的基本行为,使用它的子类才能处理具体的手势
UITapGestureRecognizer
(敲击)
UIPinchGestureRecognizer
(捏合,用于缩放)
UIPanGestureRecognizer
(拖拽)
UISwipeGestureRecognizer
(轻扫)
UIRotationGestureRecognizer
(旋转)
UILongPressGestureRecognizer
(长按)
- 手势识别的状态
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
// 没有触摸事件发生,所有手势识别的默认状态
UIGestureRecognizerStatePossible,
// 一个手势已经开始但尚未改变或者完成时
UIGestureRecognizerStateBegan,
// 手势状态改变
UIGestureRecognizerStateChanged,
// 手势完成
UIGestureRecognizerStateEnded,
// 手势取消,恢复至Possible状态
UIGestureRecognizerStateCancelled,
// 手势失败,恢复至Possible状态
UIGestureRecognizerStateFailed,
// 识别到手势识别
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
复制代码
- 具体代码
- (void)creatGestureRecognizer {
//点击手势
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(onTap:)];
//tap.numberOfTapsRequired = 2;//点几下才响应,默认值是1
//tap.numberOfTouchesRequired = 2;//几根手指点击,默认值也是一
//给视图加手势
[_view addGestureRecognizer:tap];//会增加tap的引用计数
//缩放手势(捏合手势)
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(onPinch:)];
[_view addGestureRecognizer:pinch];
pinch.delegate = self; //需要系统同时识别缩放和旋转,设置代理
//旋转手势
UIRotationGestureRecognizer *rot = [[UIRotationGestureRecognizer alloc]initWithTarget:self action:@selector(onRotata:)];
[_view addGestureRecognizer:rot];
rot.delegate = self;//需要系统同时识别缩放和旋转,设置代理
//滑动手势,每个滑动只支持(上下左右)一个方向,如果想支持多个方向就要建多个滑动手势
UISwipeGestureRecognizer *swipeDown = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(onSwipe:)];
swipeDown.direction = UISwipeGestureRecognizerDirectionDown;//设置滑动方向为向下
[_view addGestureRecognizer:swipeDown];
[swipeDown release];
UISwipeGestureRecognizer *swipeUp = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(onSwipe:)];
swipeUp.direction = UISwipeGestureRecognizerDirectionUp;//设置滑动方向为向上
[_view addGestureRecognizer:swipeUp];
//拖动手势,一个视图不能同时响应拖动滑动两种手势,如果两个都加响应后加的那个
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(onPan:)];
[_view addGestureRecognizer:pan];
//长按手势
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(onLongPress:)];
[_view addGestureRecognizer:longPress];
[longPress release];
}
- (void)onTap:(UITapGestureRecognizer *)sender {
NSLog(@"点击手势");
//用于获取手势相对于某视图的坐标
//sender.view表示sender所在的视图
CGPoint p = [sender locationInView:sender.view];
NSLog(@"%f,%f",p.x,p.y);
}
- (void)onPinch:(UIPinchGestureRecognizer *)sender {
NSLog(@"缩放手势,比例:%f",sender.scale);
//sender.scale表示缩放比例
sender.view.transform = CGAffineTransformScale(sender.view.transform, sender.scale, sender.scale);
sender.scale = 1;//告诉缩放手势以当前的比例为基准(默认值是以手势开始时的比例为基准)
}
- (void)onRotata:(UIRotationGestureRecognizer *)sender {
NSLog(@"旋转手势:%f",sender.rotation);//表示手势旋转弧度
sender.view.transform = CGAffineTransformRotate(sender.view.transform, sender.rotation);
sender.rotation = 0;//表示告诉旋转手势以当前的弧度为基准
}
- (void)onSwipe:(UISwipeGestureRecognizer *)sender {
NSLog(@"滑动手势");
if (sender.direction == UISwipeGestureRecognizerDirectionUp) {
CGPoint p = sender.view.center;
p.y -= 10;
sender.view.center = p;
}else {
CGPoint p = sender.view.center;
p.y += 10;
sender.view.center = p;
}
}
- (void)onPan:(UIPanGestureRecognizer *)sender {
//sender.state表示手势的状态:手势开始,结束,正在进行等
NSLog(@"拖动手势");
if (sender.state == UIGestureRecognizerStateBegan) {
NSLog(@"手势开始");
}else if (sender.state == UIGestureRecognizerStateChanged) {
NSLog(@"手势进行中");
}else if (sender.state == UIGestureRecognizerStateEnded) {
NSLog(@"手势结束");
}
CGPoint p = [sender locationInView:self.view];
sender.view.center = p;
}
- (void)onLongPress:(UILongPressGestureRecognizer *)sender {
NSLog(@"长按手势");
if (sender.state == UIGestureRecognizerStateBegan) {
NSLog(@"手势开始");
}else if (sender.state == UIGestureRecognizerStateChanged) {
NSLog(@"手势进行中");
}else if (sender.state == UIGestureRecognizerStateEnded) {
NSLog(@"手势结束");
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;//任何两个手势都能同时识别
}
复制代码