iOS 三种事件控制
- 触摸事件:通过触摸、手势进行触发(例如手指点击、缩放)
- 运动事件:通过加速器进行触发(例如手机晃动)
- 远程控制事件:通过其他远程设备触发(例如耳机控制按钮)
触摸事件
继承UIResponder的对象均能接收触摸事件,而常用的UIView、UIViewController、UIApplication都继承自UIResponder,因此,基本上所有的UIKit控件和视图控制器均能接收触摸事件。
触摸事件所需要的方法:
1 | //一根或多根手指开始触摸屏幕时执行; |
当执行触摸事件时,上述函数会传入 (NSSet *)touches对象,这个集合中每一个元素都是
UITouch类型的,通过
1 | UITouch *touch=[touches anyObject]; |
获取一个或者多个触摸对象。
UITouch类中包涵如下属性:
- window:触摸时所在的窗口
- view:触摸时所在视图
- tapCount:短时间内点击的次数
- timestamp:触摸产生或变化的时间戳
- phase:触摸周期内的各个状态
- locationInView:方法:取得在指定视图的位置
- previousLocationInView:方法:取得移动的前一个位置
响应者链
在视图的多层架构中,可以按照视图的叠加层次进行链式响应,如果当前视图无法对触摸事件进行响应,事件的响应者会变为当前视图的包涵视图,以此类推,从而使事件的响应者形成一个链,叫做响应者链。如果响应者链中的某个响应者处理了事件,则此事件不会继续向下传播;反之则会一直向下传播,直到此事件被丢弃为止。
响应者链中的响应者不能处理事件的条件:
- userInteractionEnabled=NO
- hidden=YES
- alpha=0~0.01
- 没有实现开始触摸方法(注意是touchesBegan:withEvent:而不是移动和结束触摸事件)
当然前三点都是针对UIView控件或其子控件而言的,第四点可以针对UIView也可以针对视图控制器等其他UIResponder子类。对于第四种情况这里再次强调是对象中重写了开始触摸方法,则会处理这个事件,如果仅仅写了移动、停止触摸或取消触摸事件(或者这三个事件都重写了)没有写开始触摸事件,则此事件该对象不会进行处理。
手势识别系统
6种基本手势类
UIKit封装了6种基本手势,分别为:
手势类 | 说明 |
---|---|
UITapGestureRecognizer | 点按手势 |
UIPinchGestureRecognizer | 捏合手势 |
UIPanGestureRecognizer | 拖动手势 |
UISwipeGestureRecognizer | 轻扫手势,支持四个方向的轻扫,但是不同的方向要分别定义轻扫手势 |
UIRotationGestureRecognizer | 旋转手势 |
UILongPressGestureRecognizer | 长按手势 |
手势类中的基本属性和方法
所有的手势操作都继承于UIGestureRecognizer,这个类本身不能直接使用。这个类中定义了这几种手势共有的一些属性和方法(下表仅列出常用属性和方法):
名称 | 说明 |
---|---|
属性 | |
@property(nonatomic,readonly) UIGestureRecognizerState state; | 手势状态 |
@property(nonatomic, getter=isEnabled) BOOL enabled; | 手势是否可用 |
@property(nonatomic,readonly) UIView *view; | 触发手势的视图(一般在触摸执行操作中我们可以通过此属性获得触摸视图进行操作) |
@property(nonatomic) BOOL delaysTouchesBegan; | 手势识别失败前不执行触摸开始事件,默认为NO;如果为YES,那么成功识别则不执行触摸开始事件,失败则执行触摸开始事件;如果为NO,则不管成功与否都执行触摸开始事件; |
方法 | |
- (void)addTarget:(id)target action:(SEL)action; | 添加触摸执行事件 |
- (void)removeTarget:(id)target action:(SEL)action; | 移除触摸执行事件 |
- (NSUInteger)numberOfTouches; | 触摸点的个数(同时触摸的手指数) |
- (CGPoint)locationInView:(UIView *)view; | 在指定视图中的相对位置 |
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(UIView*)view; | 触摸点相对于指定视图的位置 |
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer; | 指定一个手势需要另一个手势执行失败才会执行 |
代理方法 | |
- (BOOL)gestureRecognizer:(UIGestureRecognizer )gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer )otherGestureRecognizer; | 一个控件的手势识别后是否阻断手势识别继续向下传播,默认返回NO;如果为YES,响应者链上层对象触发手势识别后,如果下层对象也添加了手势并成功识别也会继续执行,否则上层对象识别后则不再继续传播; |
手势的状态
手势的状态分为以下6种:
1 | typedef NS_ENUM(NSInteger, UIGestureRecognizerState) { |
在6种基本手势中,点按属于离散型手势,其它的五种属于连续手势。离散手势无法被cancel,而且执行结果要么是fail,要么是end。下面为苹果官方的手势状态示意图,各个圆圈分别代表手势的各种状态。
下面是离散型手势和连续型手势触发效果图:
手势系统的使用方式
下面是手势系统的使用步骤:
- 创建对应的手势对象;
- 设置手势识别属性【可选】;
- 附加手势到指定的对象;
- 编写手势操作方法;
下面的代码段简述了如何使用点按手势,其它手势的使用与此基本相同,不同的是需要设置的手势识别属性。
1 | /*添加点按手势*/ |
Tips
1. 真正在点按时处理业务操作的函数是 tapGestureAction:
2. 添加手势的对象(上面代码段中为self.view)需要满足处理事件的四个条件(前文已经介绍过这四个条件了),否则添加的手势无法被触发。
手势冲突
轻扫和拖动容易产生手势冲突,拖动和长按也会产生手势冲突,解决此问题的方法就是使用方法
1 | - (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer; |
比如如下代码段:
1 | //解决在图片上滑动时拖动手势和轻扫手势的冲突,当拖动执行失败时才执行轻扫 |
多个控件均处理同一个事件
上面说过,在响应者链中如果一个响应者已经处理了事件,事件就不会继续向后传递,但是,有时需要多个控件对同一个事件作出响应,这时我们就需要做一些操作使事件被处理后仍能继续向后传递。
1 | - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer; |
使用上面这个代理方法即可实现。这个方法磨人返回NO,阻止继续向后传递事件,如果返回YES便可以继续向后传递事件。
这个代理方法在协议 UIGestureRecognizerDelegate 中。
给需要向后继续传递的手势对象指定代理:
1 | UILongPressGestureRecognizer *viewLongPressGesture=[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressView:)]; |
下面的代码段对代理方法进行实现:
1 | -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ |
此方法的第二个参数是将要被阻断的手势,然后可以通过判断这个手势的view控件是什么类型或者和其它控件的地址是否相等等方式,确定这个手势是否需要被传递下去。
运动事件
相比于触摸事件,运动事件的使用比较简单,下面是运动事件使用的三个方法:
1 | //运动开始时执行; |
监听运动事件对于UI控件有个前提就是监听对象必须是第一响应者(对于UIViewController视图控制器和UIAPPlication没有此要求)。这也就意味着如果监听的是一个UI控件那么
1 | -(BOOL)canBecomeFirstResponder; |
方法必须返回YES(可通过重写控件的此方法实现)。同时控件显示时调用视图控制器的becomeFirstResponder方法。当视图不再显示时注销第一响应者身份。
Tips
1. 控件显示时注册第一响应者可以通过重写
-(void)viewWillAppear:(BOOL)animated; 或者
-(void)viewDidAppear:(BOOL)animated; 方法实现
2. 控件不显示时注销第一响应者可以通过重写
-(void)viewDidDisappear:(BOOL)animated; 或者
-(void)viewWillDisappear:(BOOL)animated; 方法实现
因此,在运动事件中需要父视图和子控件配合完成。
子控件中重写
1 | -(BOOL)canBecomeFirstResponder; |
父视图中重写
1 | -(void)viewWillAppear:(BOOL)animated; 或者 |
远程控制事件
远程控制事件这里主要说的就是耳机线控操作。
和远程控制有关的只有下面这一个函数:
1 | //接收到远程控制消息时执行; |
要监听到这个事件有三个前提(视图控制器UIViewController或应用程序UIApplication只有两个):
- 启用远程事件接收(使用[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];方法)。
- 对于UI控件同样要求必须是第一响应者(对于视图控制器UIViewController或者应用程序UIApplication对象监听无此要求)。
- 应用程序必须是当前音频的控制者,也就是在iOS 7中通知栏中当前音频播放程序必须是我们自己开发程序。
远程控制事件只能用来控制音频,如果我们开发的应用和音频无关,但又想使用远程控制,可能需要我们在内部自己实现一个音频功能。
运动事件以及远程控制事件的事件类型:
1 | typedef NS_ENUM(NSInteger, UIEventSubtype) { |
Tips
1. 为了模拟一个真实的播放器,程序中我们启用了后台运行模式,配置方法:在info.plist中添加UIBackgroundModes并且添加一个元素值为audio。
2. 即使利用线控进行音频控制我们也无法监控到耳机增加音量、减小音量的按键操作(另外注意模拟器无法模拟远程事件,请使用真机调试)。
3. 子事件的类型跟当前音频状态有直接关系,点击一次播放/暂停按钮究竟是【播放】还是【播放/暂停】状态切换要看当前音频处于什么状态,如果处于停止状态则点击一下是播放,如果处于暂停或播放状态点击一下是暂停和播放切换。