iOS事件处理-
用户使用App产生的事件及响应方法:
iOS中不是任何对象都能处理事件,只有继承UIResponder的对象才能接受并处理事件--称为响应者对象;例如:UIApplication,UIView,UIViewController.
1.触摸事件-touch
- (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;//被动取消,可能会经历.
2.加速计事件(摇一摇)
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
3远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
1. 触摸事件:
1.1 简介:
以触摸事件为例:当一根手指或多根触摸屏幕,就会创建一个与之相关的UITouch对象,存在NSSet集合中.用touches anyObjiect即可获取.
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次 touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸
一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数
touch作用:
- 保存跟相关信息:触摸位置,事件,阶段;
- 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存当前位置.
- 手指离开屏幕,系统才会销毁相应的UItouch对象--所以,要避免使用双击事件.
UITouch属性:
- @property(nonatomic,readonly,retain) UIWindow *window;//触摸时所处窗口
- @property(nonatomic,readonly,retain) UIView *view;//触摸时所处实处
- @property(nonatomic,readonly) NSUInteger tapCount;//短时间内点击屏幕次数.
- @property(nonatomic,readonly) NSTimeInterval timestamp;//记录触摸产生或变化时的事件 /秒
- @property(nonatomic,readonly) UITouchPhase phase;//触摸事件所处状态;根据此属性,判断当前调用哪个方法.
UITouch方法:
-(CGPoint)locationInView:(UIView *) view; //返回当前触摸点,以view坐标系为准,View参数为nil的时候,默认为UIWindow上.
- (CGPoint)previousLocationInView:view //记录上一个触摸点位置
UIEvent:
每产生一个事件,就会产生一个UIEvent对象,记录事件产生的时间和类型.
属性:
@property(nonatomic,readonly) UIEventType type;//类型
@property(nonatomic,readonly) UIEventSubtype subtype;//远程控制事件的多种情况.
@property(nonatomic,readonly) NSTimeInterval timestamp; //时间
• UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)
- (nullable NSSet <UITouch *> *)allTouches;
1.2 一般使用:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
//1. 取得一个触摸对象(对于多点触摸可能有多个对象)
UITouch *touch=[touches anyObject];
//NSLog(@"%@",touch);
//2. 取得当前位置
CGPoint current=[touch locationInView:self.view];
//取得前一个位置
//CGPoint previous=[touch previousLocationInView:self.view];
//移动前的中点位置
CGPoint center=_image.center;
//移动偏移量
CGPoint offset=CGPointMake(current.x-previous.x, current.y-previous.y);
//3. 重新设置新位置
_image.center=CGPointMake(center.x+offset.x, center.y+offset.y);
2. 事件的传递和响应:
2.1 事件的传递
事件传递 过程;
发生触摸后,事件会加入到UIApplication事件队列---->UIApplication会从事件队列取出最前面的事件,分发下去处理,通常发送到App的主窗口(keyWindow);---->主窗口会调用hitTest:withEvent:方法在视图层级中找一个最合适的视图来处理触摸事件;
视图层级事件传递的 逻辑:
- 自己是否能接受触摸事件---->不能,事件传递结束.
- 触摸点是否在自己身上------>不在,事件传递结束.
- 从后往前(同级最上方的开始)遍历子控件-重复1和2的判断.只要有通过的,就继续遍历通过控件的子控件.如果没有符合条件的子控件,那么自己最合适处理.
不能接受触摸的三种情况:
- userInteractionEnabled = NO,
- hidden = YES,
- alpha < 0.01;
视图层级事件传递的 方法:
系统会调用:
hitTest: withEvent:
作用:需找最合适的View, return 这个View.
调用时候:当事件传递给控件的时候系统调用
-(UIView*)hitTest: WithEvent {
//1.判断当前控件能不能接受事件:
if(上面三个条件)
//2.判断这个点在不在当前控件上
pointInside: withEvent:
//3.从后往前遍历子控件
for i - -;取出子控件
把当前坐标系上的点转换成子控件坐标系上的点 convertPoint: toView:
让子控件找最合适view, hitTest: withEvent:
if(view) return view;
事件传递到某个控件,就会调用HitTest,我们可以重写此方法,改变事件传递链.使用情况比较少,一般用于自定义手势
上面的步骤就是点击检测的过程,其实就是查找事件触发者的过程。触摸对象并非就是事件的响应者,检测到了触摸的对象之后,事件到底是如何响应呢?这个过程就必须引入一个新的概念“响应者链”。
2.2 事件的响应
响应者链条.
作用:让多个view响应一个事件
响应者链条 (一般限限子父关系) - 当touch方法中调用父类的touch方法, 说明自己这个控件不处理事件,交还给上一个响应者,让它决定处理还是继续传递.
- 如果当前的view是控制器的view,那么控制器就是上一个响应者.直到Root控制器(结尾—最后到window->application->销毁)
- 如果不是,则父控件是上一个响应者.
事件传递:hiTest 没有控制器参与.
响应者链条: touch 如果不实现,不执行事件,系统默认找父类控件.一直到控制器--最后--> window-> app-> 如果都不响应就会销毁
3. 手势识别
3.1 简介
在iOS3.2之后苹果引入了手势识别,对于用户常用的手势操作进行了识别并封装成具体的类供开发者使用,这样在开发过程中我们就不必再自己编写算法识别用户的触摸操作了。在iOS中有六种手势操作:
手势 | 说明 |
---|---|
UITapGestureRecognizer | 点按手势 |
UIPinchGestureRecognizer | 捏合手势 |
UIPanGestureRecognizer | 拖动手势 |
UISwipeGestureRecognizer | 轻扫手势,支持四个方向的轻扫,但是不同的方向要分别定义轻扫手势 |
UIRotationGestureRecognizer | 旋转手势 |
UILongPressGestureRecognizer | 长按手势 |
* tap(代理:左边不能点,右边能点)
* longPress(allowableMovement:触发之前,最大的移动范围)
> 默认调用两次,开始一次,结束一次。
* swipe:(一个手势只能识别一个方向)
* 旋转:
基于上一次旋转
注意:通过transform形变,需要去掉autolayout,才准确
* 复位:(手势的取值都是相对最原始的位置,我们应该是需要相对上一次,因此每次调用,就复位一下,每次都是从零开始旋转角度)
缩放:复位
* 如何同时支持旋转和缩放?默认不支持多个手指,
Simultaneously:同时
当使用一个手势的时候会调用代理的Simultaneously方法,询问是否支持多个手势
* pan
获取平移的位置:translationInView
复位:setTranslation:inView: 需要传一个view,因为点的位置跟坐标系有关系,看他是基于哪个坐标系被清空的。
所有的手势操作都继承于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,响应者链上层对象触发手势识别后,如果下层对象也添加了手势并成功识别也会继续执行,否则上层对象识别后则不再继续传播; |
3.2 手势状态
在六种手势识别中,只有一种手势是离散手势,它就是UITapGestureRecgnier。离散手势特点就是一旦识别无法取消,而且只会调用一次手势操作事件. 换句话说其他五种手势是连续手势,连续手势的特点就是会多次调用手势操作事件,而且在连续手势识别后可以取消手势。
所以state-手势状态分为如下几种:
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // 尚未识别是何种手势操作(但可能已经触发了触摸事件),默认状态
UIGestureRecognizerStateBegan, // 手势已经开始,此时已经被识别,但是这个过程中可能发生变化,手势操作尚未完成
UIGestureRecognizerStateChanged, // 手势状态发生转变
UIGestureRecognizerStateEnded, // 手势识别操作完成(此时已经松开手指)
UIGestureRecognizerStateCancelled, // 手势被取消,恢复到默认状态
UIGestureRecognizerStateFailed, // 手势识别失败,恢复到默认状态
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // 手势识别完成,同UIGestureRecognizerStateEnded
};
- 对于离散手势,tap;要么被识别,要么失败.点按下去一次不松开则此时什么也不会发生,松开手指立即识别并调用操作事件,并且状态为3(已完成)
- 连续手势要复杂一些.拿旋转手势为例.如果两个手指点下去不做任何操作,此时并不能识别手势.但是已经触发了触摸开始事件此时状态为0;如果此时旋转被识别,也就会调用对于的操作时间,同时状态更新为1(手势开始.)但是状态1只有一瞬间,紧接着变为状态2,并持续; 松开手指后,此时状态变为3,并调用1次操作事件.
通过苹果官方分析图理解:
3.3 使用手势
一般用于UIView,不能add target的控件添加点击事件.
一般步骤:
- 创建对应手势对象;
- 设置手势识别属性(可选)
- 添加手势到指定对象
- 编写手势操作方法.
示例代码:
/*创建手势对象*/
UITapGestureRecognizer *tapGesture = [UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapImage:)];
/*设置手势属性*/
tapGesture.numberOfTapsRequired=1;//设置点按次数,默认为1,注意在iOS中很少用双击操作
tapGesture.numberOfTouchesRequired=1;//点按的手指数
/*添加手势到对象*/
[self.imageView addGestureRecognizer:tapGesture];
/*编写手势操作方法*/
-(void)tapImage:(UITapGestureRecognizer *)gesture {
//编写希望执行的操作.
}
//其他手势注意点:
流程基本相同,但是因为属于连续手势.操作方法会调用多次,所以需要判断其手势状态.
PS.轻扫手势虽然是连续手势但是它的操作事件只会在识别结束时调用一次,此外轻扫手势支持四个方向,但是如果要支持多个方向需要添加多个轻扫手势。
3.4 手势冲突.
在iOS中,如果一个手势A的识别部分是另一个手势B的子部分时,默认情况下A会先识别,B就无法识别了;例:拖动手势的操作事件是在手势的开始状态(状态1)识别执行的,而轻扫手势的操作事件只有在手势结束状态(状态3)才能执行,因此轻扫手势就作为了牺牲品没有被正确识别。
解决方法:
//在添加手势之后调用requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer 方法
//这个方法可以指定某个手势执行的前提是另一个手势失败才会识别执行,即指定拖动手势的执行前提为轻扫手势失败;这样一来,当我们的滑动时,系统会优先考虑轻扫手势,如果发现不是轻扫,那么会执行拖动.
[panGesture requireGestureRecognizerToFail:swipeGestureToRight];
//解决滑动与右轻扫冲突.
3.5 两个不同控件的手势同时执行
在iOS触摸事件中,事件触发时根据响应者链条进行的,上层触摸事件执行后就不会向下传播;默认情况下手势也是类似的,先识别的手势会阻断手势识别操作继续传播。那么.如何让两个有层次的关系并且都添加了手势的控件都能正确识别手势呢?--自己做不到...找代理.
//手势代理方法.
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if ([otherGestureRecognizer.view isKindOfClass:[UIImageView class]])
{return YES;} //表示只有在UIImageView的手势才能向下传播.
return NO;
}
4. 加速计事件-运动事件
4.1 简介
iOS中和运动相关的有三个事件:开始运动、结束运动、取消运动。
监听运动事件对于UI控件有个前提: 监听对象必须是第一响应者.(对于UIViewController视图控制器和UIApplication没有此要求);所以 如果简体一个UI控件,那么使之-(BOOL)canBecomeFirstResponder 返回YES;然后设置为第一响应者
注意:设置第一响应者后,如果控件不显示了,要注销控件的第一响应者身份.所以常规设置响应者的代码如下:
KCImageView中
//设置控件可以成为第一响应者
-(BOOL)canBecomeFirstResponder {
return YES;
}
#pragma mark 运动开始
-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event{
//这里只处理摇晃事件
if (motion==UIEventSubtypeMotionShake) {
self.image=[self getImage];
}
}
#pragma mark 运动结束
-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event{
}
控制器中
#pragma mark 视图显示时让控件变成第一响应者
-(void)viewDidAppear:(BOOL)animated{
_imageView=[[KCImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
_imageView.userInteractionEnabled=true;
[self.view addSubview:_imageView];
[_imageView becomeFirstResponder];
}
#pragma mark 视图不显示时注销控件第一响应者的身份
-(void)viewDidDisappear:(BOOL)animated{
[_imageView resignFirstResponder];
}
5. 远程控制事件
5.1 简介
远程控制,远程控制事件这里主要说的就是耳机线控操作。
监听远程控制事件有三个前提:
- 启用远程事件接收.(使用[UIApplication shareApplication] beginReceivingRemoteControlEvents;方法);
- 对于UI控件同样要求必须是第一响应者(对于视图控制器UIViewController或者应用程序UIApplication对象监听无此要求)。
- 应用程序必须是当前音频的控制者,也就是在iOS 7中通知栏中当前音频播放程序必须是我们自己开发程序。
基于第三点我们必须明确,如果我们的程序不想要控制音频,只是想利用远程控制事件做其他的事情,例如模仿iOS7中的按音量+键拍照是做不到的,目前iOS7给我们的远程控制权限还仅限于音频控制(当然假设我们确实想要做一个和播放音频无关的应用但是又想进行远程控制,也可以隐藏一个音频播放操作,拿到远程控制操作权后进行远程控制)。
运动事件中我们也提到一个枚举类型UIEventSubtype,而且我们利用它来判断是否运动事件,在枚举中还包含了我们运程控制的子事件类型,我们先来熟悉一下这个枚举(从远程控制子事件类型也不难发现它和音频播放有密切关系):
详细内容参考博客
抽屉效果
步骤思路
添加子视图
* 简单的滑动效果
* 监听控制器处理事件方法
* 获取x轴偏移量
* 改变主视图的frame
* 利用KVO做视图切换
往左移动,显示右边,隐藏左边
往右移动,显示左边,隐藏右边
* 复杂的滑动效果,PPT讲解(根据手指每移动一点,x轴的偏移量算出当前视图的frame)
假设x移到320时,y移动到60,算出没移动一点x,移动多少y
offsetY = offsetX * 60 / 320 手指每移动一点,x轴偏移量多少,y偏移多少
为了好看,x移动到320,距离上下的高度需要保持一致,而且有一定的比例去缩放他的尺寸。
怎么根据之前的frame,算出当前的frame,touchMove只能拿到之前的frame.
当前的高度 = 之前的高度 * 这个比例
缩放比例:当前的高度/之前的高度 (screenH - 2 * offsetY) / screenH
当前的宽度也一样求。
y值,计算比较特殊,不能直接用之前的y,加上offsetY,往左滑动,主视图应该往下走,但是offsetX是负数,导致主视图会往上走。
y = (screenH - 当前的高度)* 0.5
getCurrentFrameWithOffsetX
* 定位(滑动松开手指的时候,移动到目标点)
移动到左右目标点,根据偏移量 = 当前目标点的x - 之前视图的x,计算移动到目标点的frame
还原:当没有移动到目标点,就把主视图还原。
* 复位(当主视图不在原始的位置,点击屏幕,恢复原来位置)
判断手指是否移动,移动的时候就自动定位,不需要手动复位。