-- iOS事件全面解析html
iPhone的成功很大一部分得益于它多点触摸的强大功能,乔布斯让人们认识到手机实际上是能够不用按键和手写笔直接操做的,这不愧为一项伟大的设计。今天咱们就针对iOS的触摸事件(手势操做)、运动事件、远程控制事件等展开学习:ios
在iOS中事件分为三类:算法
下图是苹果官方对于这三种事件的形象描述:数组
在iOS中并非全部的类都能处理接收并事件,只有继承自UIResponder类的对象才能处理事件(如咱们经常使用的UIView、UIViewController、UIApplication都继承自UIResponder,它们都能接收并处理事件)。在UIResponder中定义了上面三类事件相关的处理方法:session
事件 | 说明 |
---|---|
触摸事件 | |
- (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; | 触摸意外取消时执行(例如正在触摸时打入电话); |
运动事件 | |
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0); | 运动开始时执行; |
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0); | 运动结束后执行; |
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0); | 运动被意外取消时执行; |
远程控制事件 | |
- (void)remoteControlReceivedWithEvent:(UIEvent *)event NS_AVAILABLE_IOS(4_0); | 接收到远程控制消息时执行; |
三类事件中触摸事件在iOS中是最经常使用的事件,这里咱们首先介绍触摸事件。app
在下面的例子中定义一个KCImage,它继承于UIView,在KCImage中指定一个图片做为背景。定义一个视图控制器KCTouchEventViewController,而且在其中声明一个KCImage变量,添加到视图控制器中。既然UIView和UIViewController都继承于UIResponder,那么也就就意味着全部的UIKit控件和视图控制器均能接收触摸事件。首先咱们在KCTouchEventViewController中添加触摸事件,并利用触摸移动事件来移动KCImage,具体代码以下:dom
// // KCTouchEvenViewController.m // TouchEventAndGesture // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCTouchEvenViewController.h" #import "KCImage.h" @interface KCTouchEvenViewController (){ KCImage *_image; } @end @implementation KCTouchEvenViewController - (void)viewDidLoad { [super viewDidLoad]; _image=[[KCImage alloc]initWithFrame:CGRectMake(50, 50, 150, 169 )]; //_image.userInteractionEnabled=NO; [self.view addSubview:_image]; } #pragma mark - 视图控制器的触摸事件 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"UIViewController start touch..."); } -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ //取得一个触摸对象(对于多点触摸可能有多个对象) UITouch *touch=[touches anyObject]; //NSLog(@"%@",touch); //取得当前位置 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); //从新设置新位置 _image.center=CGPointMake(center.x+offset.x, center.y+offset.y); NSLog(@"UIViewController moving..."); } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"UIViewController touch end."); } @end
如今运行程序:ide
上面示例中咱们用到了UITouch类,当执行触摸事件时会将这个对象传入。在这个对象中包含了触摸的全部信息:布局
从上面运行效果能够看到不管是选择KCImage拖动仍是在界面其余任意位置拖动都能达到移动图片的效果。既然KCImage是UIView固然在KCImage中也能触发相应的触摸事件,假设在KCImage中定义三个对应的事件:学习
// // KCImage.m // TouchEventAndGesture // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCImage.h" @implementation KCImage - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { UIImage *img=[UIImage imageNamed:@"photo.png"]; [self setBackgroundColor:[UIColor colorWithPatternImage:img]]; } return self; } #pragma mark - UIView的触摸事件 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"UIView start touch..."); } -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"UIView moving..."); } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"UIView touch end."); } @end
此时若是运行程序会发现若是拖动KCImage没法达到预期的效果,可是能够发现此时会调用KCImage的触摸事件而不会调用KCTouchEventViewController中的触摸事件。若是直接拖拽其余空白位置则能够正常拖拽,并且从输出信息能够发现此时调用的是视图控制器的触摸事件。这是为何呢?要解答这个问题咱们须要了解iOS中事件的处理机制。
在iOS中发生触摸后,事件会加入到UIApplication事件队列(在这个系列关于iOS开发的第一篇文章中咱们分析iOS程序原理的时候就说过程序运行后UIApplication会循环监听用户操做),UIApplication会从事件队列取出最前面的事件并分发处理,一般先分发给应用程序主窗口,主窗口会调用hitTest:withEvent:方法(假设称为方法A,注意这是UIView的方法),查找合适的事件触发视图(这里一般称为“hit-test view”):
上面的步骤就是点击检测的过程,其实就是查找事件触发者的过程。触摸对象并不是就是事件的响应者(例如上面第一个例子中没有重写KCImage触摸事件时,KCImge做为触摸对象,可是事件响应者倒是UIViewController),检测到了触摸的对象以后,事件究竟是如何响应呢?这个过程就必须引入一个新的概念“响应者链”。
什么是响应者链呢?咱们知道在iOS程序中不管是最后面的UIWindow仍是最前面的某个按钮,它们的摆放是有先后关系的,一个控件能够放到另外一个控件上面或下面,那么用户点击某个控件时是触发上面的控件仍是下面的控件呢,这种前后关系构成一个链条就叫“响应者链”。在iOS中响应者链的关系能够用下图表示:
当一个事件发生后首先看initial view可否处理这个事件,若是不能则会将事件传递给其上级视图(inital view的superView);若是上级视图仍然没法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;若是不能则接着判断该视图控制器可否处理此事件,若是仍是不能则继续向上传递;(对于第二个图视图控制器自己还在另外一个视图控制器中,则继续交给父视图控制器的根视图,若是根视图不能处理则交给父视图控制器处理);一直到window,若是window仍是不能处理此事件则继续交给application(UIApplication单例对象)处理,若是最后application仍是不能处理此事件则将其丢弃。
这个过程你们理解起来并不难,关键问题是在这个过程当中各个对象如何知道本身能不能处理该事件呢?对于继承UIResponder的对象,其不能处理事件有几个条件:
固然前三点都是针对UIView控件或其子控件而言的,第四点能够针对UIView也能够针对视图控制器等其余UIResponder子类。对于第四种状况这里再次强调是对象中重写了开始触摸方法,则会处理这个事件,若是仅仅写了移动、中止触摸或取消触摸事件(或者这三个事件都重写了)没有写开始触摸事件,则此事件该对象不会进行处理。
相信到了这里你们对于上面点击图片为何不能拖拽已经很明确了。事实上经过前面的解释你们应该能够猜到即便KCImage实现了开始拖拽方法,若是在KCTouchEventViewController中设置KCImage对象的userInteractionEnabled为NO也是能够拖拽的。
注意:上面提到hitTest:withEvent:能够指定触发事件的视图,这里就再也不举例说明,这个方法重写状况比较少,通常用于自定义手势,有兴趣的童鞋能够访问:Event Delivery: The Responder Chain。
经过前面的内容咱们能够看到触摸事件使用起来比较容易,可是对于多个手指触摸并进行不一样的变化操做就要复杂的多了。例如说若是两个手指捏合,咱们虽然在触摸开始、移动等事件中能够经过UITouchs获得两个触摸对象,可是咱们如何能判断用户是用两个手指捏合仍是横扫或者拖动呢?在iOS3.2以后苹果引入了手势识别,对于用户经常使用的手势操做进行了识别并封装成具体的类供开发者使用,这样在开发过程当中咱们就没必要再本身编写算法识别用户的触摸操做了。在iOS中有六种手势操做:
手势 | 说明 |
---|---|
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,响应者链上层对象触发手势识别后,若是下层对象也添加了手势并成功识别也会继续执行,不然上层对象识别后则再也不继续传播; |
这里着重解释一下上表中手势状态这个对象。在六种手势识别中,只有一种手势是离散手势,它就是UITapGestureRecgnier。离散手势的特色就是一旦识别就没法取消,并且只会调用一次手势操做事件(初始化手势时指定的触发方法)。换句话说其余五种手势是连续手势,连续手势的特色就是会屡次调用手势操做事件,并且在连续手势识别后能够取消手势。从下图能够看出二者调用操做事件的次数是不一样的:
在iOS中将手势状态分为以下几种:
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) { UIGestureRecognizerStatePossible, // 还没有识别是何种手势操做(但可能已经触发了触摸事件),默认状态 UIGestureRecognizerStateBegan, // 手势已经开始,此时已经被识别,可是这个过程当中可能发生变化,手势操做还没有完成 UIGestureRecognizerStateChanged, // 手势状态发生转变 UIGestureRecognizerStateEnded, // 手势识别操做完成(此时已经松开手指) UIGestureRecognizerStateCancelled, // 手势被取消,恢复到默认状态 UIGestureRecognizerStateFailed, // 手势识别失败,恢复到默认状态 UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // 手势识别完成,同UIGestureRecognizerStateEnded };
为了你们更好的理解这个状态的变化,不妨在操做事件中打印事件状态,会发如今操做事件中的状态永远不可能为0(默认状态),由于只要调用此事件说明已经被识别了。前面也说过,手势识别从根本仍是调用触摸事件而完成的,连续手势之因此会发生状态转换彻底是因为触摸事件中的移动事件形成的,没有移动事件也就不存在这个过程当中状态变化。
你们经过苹果官方的分析图再理解一下上面说的内容:
在iOS中添加手势比较简单,能够概括为如下几个步骤:
为了帮助你们理解,下面以一个图片查看程序演示一下上面几种手势,在这个程序中咱们完成如下功能:
若是点按图片会在导航栏显示图片名称;
若是长按图片会显示删除按钮,提示用户是否删除;
若是捏合会放大、缩小图片;
若是轻扫会切换到下一张或上一张图片;
若是旋转会旋转图片;
若是拖动会移动图片;
具体布局草图以下:
为了显示导航条,咱们首先将主视图控制器KCPhotoViewController放入一个导航控制器,而后在主视图控制器中放一个UIImage用于展现图片。下面是主要代码:
// // KCGestureViewController.m // TouchEventAndGesture // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCPhotoViewController.h" #define kImageCount 3 @interface KCPhotoViewController (){ UIImageView *_imageView;//图片展现控件 int _currentIndex;//当前图片索引 } @end @implementation KCPhotoViewController - (void)viewDidLoad { [super viewDidLoad]; [self initLayout]; [self addGesture]; } #pragma mark 布局 -(void)initLayout{ /*添加图片展现控件*/ CGSize screenSize=[UIScreen mainScreen].applicationFrame.size; CGFloat topPadding=20; CGFloat y=22+44+topPadding,height=screenSize.height-y-topPadding; CGRect imageFrame=CGRectMake(0, y, screenSize.width, height); _imageView=[[UIImageView alloc]initWithFrame:imageFrame]; _imageView.contentMode=UIViewContentModeScaleToFill;//设置内容模式为缩放填充 _imageView.userInteractionEnabled=YES;//这里必须设置为YES,不然没法接收手势操做 [self.view addSubview:_imageView]; //添加默认图片 UIImage *image=[UIImage imageNamed:@"0.jpg"]; _imageView.image=image; [self showPhotoName]; } #pragma mark 添加手势 -(void)addGesture{ /*添加点按手势*/ //建立手势对象 UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapImage:)]; //设置手势属性 tapGesture.numberOfTapsRequired=1;//设置点按次数,默认为1,注意在iOS中不多用双击操做 tapGesture.numberOfTouchesRequired=1;//点按的手指数 //添加手势到对象(注意,这里添加到了控制器视图中,而不是图片上,不然点击空白没法隐藏导航栏) [self.view addGestureRecognizer:tapGesture]; /*添加长按手势*/ UILongPressGestureRecognizer *longPressGesture=[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressImage:)]; longPressGesture.minimumPressDuration=0.5;//设置长按时间,默认0.5秒,通常这个值不要修改 //注意因为咱们要作长按提示删除操做,所以这个手势再也不添加到控制器视图上而是添加到了图片上 [_imageView addGestureRecognizer:longPressGesture]; /*添加捏合手势*/ UIPinchGestureRecognizer *pinchGesture=[[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinchImage:)]; [self.view addGestureRecognizer:pinchGesture]; /*添加旋转手势*/ UIRotationGestureRecognizer *rotationGesture=[[UIRotationGestureRecognizer alloc]initWithTarget:self action:@selector(rotateImage:)]; [self.view addGestureRecognizer:rotationGesture]; /*添加拖动手势*/ UIPanGestureRecognizer *panGesture=[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panImage:)]; [_imageView addGestureRecognizer:panGesture]; /*添加轻扫手势*/ //注意一个轻扫手势只能控制一个方向,默认向右,经过direction进行方向控制 UISwipeGestureRecognizer *swipeGestureToRight=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeImage:)]; //swipeGestureToRight.direction=UISwipeGestureRecognizerDirectionRight;//默认为向右轻扫 [self.view addGestureRecognizer:swipeGestureToRight]; UISwipeGestureRecognizer *swipeGestureToLeft=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeImage:)]; swipeGestureToLeft.direction=UISwipeGestureRecognizerDirectionLeft; [self.view addGestureRecognizer:swipeGestureToLeft]; } #pragma mark 显示图片名称 -(void)showPhotoName{ NSString *title=[NSString stringWithFormat:@"%i.jpg",_currentIndex]; [self setTitle:title]; } #pragma mark 下一张图片 -(void)nextImage{ int index=(_currentIndex+kImageCount+1)%kImageCount; NSString *imageName=[NSString stringWithFormat:@"%i.jpg",index]; _imageView.image=[UIImage imageNamed:imageName]; _currentIndex=index; [self showPhotoName]; } #pragma mark 上一张图片 -(void)lastImage{ int index=(_currentIndex+kImageCount-1)%kImageCount; NSString *imageName=[NSString stringWithFormat:@"%i.jpg",index]; _imageView.image=[UIImage imageNamed:imageName]; _currentIndex=index; [self showPhotoName]; } #pragma mark - 手势操做 #pragma mark 点按隐藏或显示导航栏 -(void)tapImage:(UITapGestureRecognizer *)gesture{ //NSLog(@"tap:%i",gesture.state); BOOL hidden=!self.navigationController.navigationBarHidden; [self.navigationController setNavigationBarHidden:hidden animated:YES]; } #pragma mark 长按提示是否删除 -(void)longPressImage:(UILongPressGestureRecognizer *)gesture{ //NSLog(@"longpress:%i",gesture.state); //注意其实在手势里面有一个view属性能够获取点按的视图 //UIImageView *imageView=(UIImageView *)gesture.view; //因为连续手势此方法会调用屡次,因此须要判断其手势状态 if (gesture.state==UIGestureRecognizerStateBegan) { UIActionSheet *actionSheet=[[UIActionSheet alloc]initWithTitle:@"System Info" delegate:nil cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete the photo" otherButtonTitles:nil]; [actionSheet showInView:self.view]; } } #pragma mark 捏合时缩放图片 -(void)pinchImage:(UIPinchGestureRecognizer *)gesture{ //NSLog(@"pinch:%i",gesture.state); if (gesture.state==UIGestureRecognizerStateChanged) { //捏合手势中scale属性记录的缩放比例 _imageView.transform=CGAffineTransformMakeScale(gesture.scale, gesture.scale); }else if(gesture.state==UIGestureRecognizerStateEnded){//结束后恢复 [UIView animateWithDuration:.5 animations:^{ _imageView.transform=CGAffineTransformIdentity;//取消一切形变 }]; } } #pragma mark 旋转图片 -(void)rotateImage:(UIRotationGestureRecognizer *)gesture{ //NSLog(@"rotate:%i",gesture.state); if (gesture.state==UIGestureRecognizerStateChanged) { //旋转手势中rotation属性记录了旋转弧度 _imageView.transform=CGAffineTransformMakeRotation(gesture.rotation); }else if(gesture.state==UIGestureRecognizerStateEnded){ [UIView animateWithDuration:0.8 animations:^{ _imageView.transform=CGAffineTransformIdentity;//取消形变 }]; } } #pragma mark 拖动图片 -(void)panImage:(UIPanGestureRecognizer *)gesture{ if (gesture.state==UIGestureRecognizerStateChanged) { CGPoint translation=[gesture translationInView:self.view];//利用拖动手势的translationInView:方法取得在相对指定视图(这里是控制器根视图)的移动 _imageView.transform=CGAffineTransformMakeTranslation(translation.x, translation.y); }else if(gesture.state==UIGestureRecognizerStateEnded){ [UIView animateWithDuration:0.5 animations:^{ _imageView.transform=CGAffineTransformIdentity; }]; } } #pragma mark 轻扫则查看下一张或上一张 //注意虽然轻扫手势是连续手势,可是只有在识别结束才会触发,不用判断状态 -(void)swipeImage:(UISwipeGestureRecognizer *)gesture{ // NSLog(@"swip:%i",gesture.state); // if (gesture.state==UIGestureRecognizerStateEnded) { //direction记录的轻扫的方向 if (gesture.direction==UISwipeGestureRecognizerDirectionRight) {//向右 [self nextImage]; // NSLog(@"right"); }else if(gesture.direction==UISwipeGestureRecognizerDirectionLeft){//向左 // NSLog(@"left"); [self lastImage]; } // } } //-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ // //NSLog(@"touch begin..."); //} @end
运行效果:
在上面示例中须要强调几点:
细心的童鞋会发如今上面的演示效果图中当切换到下一张或者上一张图片时并无轻扫图片而是在空白地方轻扫完成,缘由是若是我轻扫图片会引发拖动手势而不是轻扫手势。换句话说,两种手势发生了冲突。
冲突的缘由很简单,拖动手势的操做事件是在手势的开始状态(状态1)识别执行的,而轻扫手势的操做事件只有在手势结束状态(状态3)才能执行,所以轻扫手势就做为了牺牲品没有被正确识别。咱们理想的状况固然是若是在图片上拖动就移动图片,若是在图片上轻扫就翻动图片。如何解决这个冲突呢?
在iOS中,若是一个手势A的识别部分是另外一个手势B的子部分时,默认状况下A就会先识别,B就没法识别了。要解决这个冲突能够利用- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;方法来完成。正是前面表格中UIGestureRecognizer的最后一个方法,这个方法能够指定某个手势执行的前提是另外一个手势失败才会识别执行。也就是说若是咱们指定拖动手势的执行前提为轻扫手势失败就能够了,这样一来当咱们手指轻轻滑动时系统会优先考虑轻扫手势,若是最后发现该操做不是轻扫,那么就会执行拖动。只要将下面的代码添加到添加手势以后就能解决这个问题了(注意为了更加清晰的区分拖动和轻扫[模拟器中拖动稍微快一点就识别成了轻扫],这里将长按手势的前提设置为拖动失败,避免演示拖动时长按手势会被识别):
//解决在图片上滑动时拖动手势和轻扫手势的冲突 [panGesture requireGestureRecognizerToFail:swipeGestureToRight]; [panGesture requireGestureRecognizerToFail:swipeGestureToLeft]; //解决拖动和长按手势之间的冲突 [longPressGesture requireGestureRecognizerToFail:panGesture];
运行效果:
咱们知道在iOS的触摸事件中,事件触发是根据响应者链进行的,上层触摸事件执行后就再也不向下传播。默认状况下手势也是相似的,先识别的手势会阻断手势识别操做继续传播。那么如何让两个有层次关系而且都添加了手势的控件都能正确识别手势呢?答案就是利用代理的-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer方法。这个代理方法默认返回NO,会阻断继续向下识别手势,若是返回YES则能够继续向下传播识别。
下面的代码控制演示了当在图片上长按时同时能够识别控制器视图的长按手势(注意其中咱们还控制了只有在UIImageView中操做的手势才能向下传递,若是不控制则全部控件均可以向下传递)
// // KCGestureViewController.m // TouchEventAndGesture // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCPhotoViewController.h" #define kImageCount 3 @interface KCPhotoViewController ()<UIGestureRecognizerDelegate>{ UIImageView *_imageView;//图片展现控件 int _currentIndex;//当前图片索引 } @end @implementation KCPhotoViewController - (void)viewDidLoad { [super viewDidLoad]; [self initLayout]; [self addGesture]; } #pragma mark 布局 -(void)initLayout{ /*添加图片展现控件*/ CGSize screenSize=[UIScreen mainScreen].applicationFrame.size; CGFloat topPadding=20; CGFloat y=22+44+topPadding,height=screenSize.height-y-topPadding; CGRect imageFrame=CGRectMake(0, y, screenSize.width, height); _imageView=[[UIImageView alloc]initWithFrame:imageFrame]; _imageView.contentMode=UIViewContentModeScaleToFill;//设置内容模式为缩放填充 _imageView.userInteractionEnabled=YES;//这里必须设置位YES,不然没法接收手势操做 //_imageView.multipleTouchEnabled=YES;//支持多点触摸,默认就是YES [self.view addSubview:_imageView]; //添加默认图片 UIImage *image=[UIImage imageNamed:@"0.jpg"]; _imageView.image=image; [self showPhotoName]; } #pragma mark 添加手势 -(void)addGesture{ /*添加点按手势*/ //建立手势对象 UITapGestureRecognizer *tapGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapImage:)]; //设置手势属性 tapGesture.numberOfTapsRequired=1;//设置点按次数,默认为1,注意在iOS中不多用双击操做 tapGesture.numberOfTouchesRequired=1;//点按的手指数 //添加手势到对象(注意,这里添加到了控制器视图中,而不是图片上,不然点击空白没法隐藏导航栏) [self.view addGestureRecognizer:tapGesture]; /*添加长按手势*/ UILongPressGestureRecognizer *longPressGesture=[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressImage:)]; longPressGesture.minimumPressDuration=0.5;//设置长按时间,默认0.5秒,通常这个值不要修改 //注意因为咱们要作长按提示删除操做,所以这个手势再也不添加到控制器视图上而是添加到了图片上 [_imageView addGestureRecognizer:longPressGesture]; /*添加捏合手势*/ UIPinchGestureRecognizer *pinchGesture=[[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinchImage:)]; [self.view addGestureRecognizer:pinchGesture]; /*添加旋转手势*/ UIRotationGestureRecognizer *rotationGesture=[[UIRotationGestureRecognizer alloc]initWithTarget:self action:@selector(rotateImage:)]; [self.view addGestureRecognizer:rotationGesture]; /*添加拖动手势*/ UIPanGestureRecognizer *panGesture=[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panImage:)]; [_imageView addGestureRecognizer:panGesture]; /*添加轻扫手势*/ //注意一个轻扫手势只能控制一个方向,默认向右,经过direction进行方向控制 UISwipeGestureRecognizer *swipeGestureToRight=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeImage:)]; //swipeGestureToRight.direction=UISwipeGestureRecognizerDirectionRight;//默认位向右轻扫 [self.view addGestureRecognizer:swipeGestureToRight]; UISwipeGestureRecognizer *swipeGestureToLeft=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeImage:)]; swipeGestureToLeft.direction=UISwipeGestureRecognizerDirectionLeft; [self.view addGestureRecognizer:swipeGestureToLeft]; //解决在图片上滑动时拖动手势和轻扫手势的冲突 [panGesture requireGestureRecognizerToFail:swipeGestureToRight]; [panGesture requireGestureRecognizerToFail:swipeGestureToLeft]; //解决拖动和长按手势之间的冲突 [longPressGesture requireGestureRecognizerToFail:panGesture]; /*演示不一样视图的手势同时执行 *在上面_imageView已经添加了长按手势,这里给视图控制器的视图也加上长按手势让二者都执行 * */ self.view.tag=100; _imageView.tag=200; UILongPressGestureRecognizer *viewLongPressGesture=[[UILongPressGestureRecognizer alloc]initWithTarget:self action:@selector(longPressView:)]; viewLongPressGesture.delegate=self; [self.view addGestureRecognizer:viewLongPressGesture]; } #pragma mark 显示图片名称 -(void)showPhotoName{ NSString *title=[NSString stringWithFormat:@"%i.jpg",_currentIndex]; [self setTitle:title]; } #pragma mark 下一张图片 -(void)nextImage{ int index=(_currentIndex+kImageCount+1)%kImageCount; NSString *imageName=[NSString stringWithFormat:@"%i.jpg",index]; _imageView.image=[UIImage imageNamed:imageName]; _currentIndex=index; [self showPhotoName]; } #pragma mark 上一张图片 -(void)lastImage{ int index=(_currentIndex+kImageCount-1)%kImageCount; NSString *imageName=[NSString stringWithFormat:@"%i.jpg",index]; _imageView.image=[UIImage imageNamed:imageName]; _currentIndex=index; [self showPhotoName]; } #pragma mark - 手势操做 #pragma mark 点按隐藏或显示导航栏 -(void)tapImage:(UITapGestureRecognizer *)gesture{ //NSLog(@"tap:%i",gesture.state); BOOL hidden=!self.navigationController.navigationBarHidden; [self.navigationController setNavigationBarHidden:hidden animated:YES]; } #pragma mark 长按提示是否删除 -(void)longPressImage:(UILongPressGestureRecognizer *)gesture{ //NSLog(@"longpress:%i",gesture.state); //注意其实在手势里面有一个view属性能够获取点按的视图 //UIImageView *imageView=(UIImageView *)gesture.view; //因为连续手势此方法会调用屡次,因此需求判断其手势状态 if (gesture.state==UIGestureRecognizerStateBegan) { UIActionSheet *actionSheet=[[UIActionSheet alloc]initWithTitle:@"System Info" delegate:nil cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete the photo" otherButtonTitles:nil]; [actionSheet showInView:self.view]; } } #pragma mark 捏合时缩放图片 -(void)pinchImage:(UIPinchGestureRecognizer *)gesture{ //NSLog(@"pinch:%i",gesture.state); if (gesture.state==UIGestureRecognizerStateChanged) { //捏合手势中scale属性记录的缩放比例 _imageView.transform=CGAffineTransformMakeScale(gesture.scale, gesture.scale); }else if(gesture.state==UIGestureRecognizerStateEnded){//结束后恢复 [UIView animateWithDuration:.5 animations:^{ _imageView.transform=CGAffineTransformIdentity;//取消一切形变 }]; } } #pragma mark 旋转图片 -(void)rotateImage:(UIRotationGestureRecognizer *)gesture{ //NSLog(@"rotate:%i",gesture.state); if (gesture.state==UIGestureRecognizerStateChanged) { //旋转手势中rotation属性记录了旋转弧度 _imageView.transform=CGAffineTransformMakeRotation(gesture.rotation); }else if(gesture.state==UIGestureRecognizerStateEnded){ [UIView animateWithDuration:0.8 animations:^{ _imageView.transform=CGAffineTransformIdentity;//取消形变 }]; } } #pragma mark 拖动图片 -(void)panImage:(UIPanGestureRecognizer *)gesture{ if (gesture.state==UIGestureRecognizerStateChanged) { CGPoint translation=[gesture translationInView:self.view];//利用拖动手势的translationInView:方法取得在相对指定视图(控制器根视图)的移动 _imageView.transform=CGAffineTransformMakeTranslation(translation.x, translation.y); }else if(gesture.state==UIGestureRecognizerStateEnded){ [UIView animateWithDuration:0.5 animations:^{ _imageView.transform=CGAffineTransformIdentity; }]; } } #pragma mark 轻扫则查看下一张或上一张 //注意虽然轻扫手势是连续手势,可是只有在识别结束才会触发,不用判断状态 -(void)swipeImage:(UISwipeGestureRecognizer *)gesture{ // NSLog(@"swip:%i",gesture.state); // if (gesture.state==UIGestureRecognizerStateEnded) { //direction记录的轻扫的方向 if (gesture.direction==UISwipeGestureRecognizerDirectionRight) {//向右 [self nextImage]; // NSLog(@"right"); }else if(gesture.direction==UISwipeGestureRecognizerDirectionLeft){//向左 // NSLog(@"left"); [self lastImage]; } // } } #pragma mark 控制器视图的长按手势 -(void)longPressView:(UILongPressGestureRecognizer *)gesture{ NSLog(@"view long press!"); } #pragma mark 手势代理方法 -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{ //NSLog(@"%i,%i",gestureRecognizer.view.tag,otherGestureRecognizer.view.tag); //注意,这里控制只有在UIImageView中才能向下传播,其余状况不容许 if ([otherGestureRecognizer.view isKindOfClass:[UIImageView class]]) { return YES; } return NO; } #pragma mark - 触摸事件 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"touch begin..."); } -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{ NSLog(@"touch end."); } @end
前面咱们主要介绍了触摸事件以及由触摸事件引出的手势识别,下面咱们简单介绍一下运动事件。在iOS中和运动相关的有三个事件:开始运动、结束运动、取消运动。
监听运动事件对于UI控件有个前提就是监听对象必须是第一响应者(对于UIViewController视图控制器和UIAPPlication没有此要求)。这也就意味着若是监听的是一个UI控件那么-(BOOL)canBecomeFirstResponder;方法必须返回YES。同时控件显示时(在-(void)viewWillAppear:(BOOL)animated;事件中)调用视图控制器的becomeFirstResponder方法。当视图再也不显示时(在-(void)viewDidDisappear:(BOOL)animated;事件中)注销第一响应者身份。
因为视图控制器默认就能够调用运动开始、运动结束事件在此再也不举例。如今不妨假设咱们如今在开发一个摇一摇找人的功能,这里咱们就自定义一个图片展现控件,在这个图片控件中咱们能够经过摇晃随机切换界面图片。代码比较简单:
KCImageView.m
// // KCImageView.m // TouchEventAndGesture // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCImageView.h" #define kImageCount 3 @implementation KCImageView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.image=[self getImage]; } return self; } #pragma mark 设置控件能够成为第一响应者 -(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 随机取得图片 -(UIImage *)getImage{ int index= arc4random()%kImageCount; NSString *imageName=[NSString stringWithFormat:@"avatar%i.png",index]; UIImage *image=[UIImage imageNamed:imageName]; return image; } @end
KCShakeViewController.m
// // KCShakeViewController.m // TouchEventAndGesture // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCShakeViewController.h" #import "KCImageView.h" @interface KCShakeViewController (){ KCImageView *_imageView; } @end @implementation KCShakeViewController - (void)viewDidLoad { [super viewDidLoad]; } #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]; } /*视图控制器的运动事件*/ //-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event{ // NSLog(@"motion begin..."); //} // //-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event{ // NSLog(@"motion end."); //} @end
运行效果(下图演示时使用了模拟器摇晃操做的快捷键,没有使用鼠标操做):
在今天的文章中还剩下最后一类事件:远程控制,远程控制事件这里主要说的就是耳机线控操做。在前面的事件列表中,你们能够看到在iOS中和远程控制事件有关的只有一个- (void)remoteControlReceivedWithEvent:(UIEvent *)event NS_AVAILABLE_IOS(4_0);事件。要监听到这个事件有三个前提(视图控制器UIViewController或应用程序UIApplication只有两个)
基于第三点咱们必须明确,若是咱们的程序不想要控制音频,只是想利用远程控制事件作其余的事情,例如模仿iOS7中的按音量+键拍照是作不到的,目前iOS7给咱们的远程控制权限还仅限于音频控制(固然假设咱们确实想要作一个和播放音频无关的应用可是又想进行远程控制,也能够隐藏一个音频播放操做,拿到远程控制操做权后进行远程控制)。
运动事件中咱们也提到一个枚举类型UIEventSubtype,并且咱们利用它来判断是否运动事件,在枚举中还包含了咱们运程控制的子事件类型,咱们先来熟悉一下这个枚举(从远程控制子事件类型也不难发现它和音频播放有密切关系):
typedef NS_ENUM(NSInteger, UIEventSubtype) { // 不包含任何子事件类型 UIEventSubtypeNone = 0, // 摇晃事件(从iOS3.0开始支持此事件) UIEventSubtypeMotionShake = 1, //远程控制子事件类型(从iOS4.0开始支持远程控制事件) //播放事件【操做:中止状态下,按耳机线控中间按钮一下】 UIEventSubtypeRemoteControlPlay = 100, //暂停事件 UIEventSubtypeRemoteControlPause = 101, //中止事件 UIEventSubtypeRemoteControlStop = 102, //播放或暂停切换【操做:播放或暂停状态下,按耳机线控中间按钮一下】 UIEventSubtypeRemoteControlTogglePlayPause = 103, //下一曲【操做:按耳机线控中间按钮两下】 UIEventSubtypeRemoteControlNextTrack = 104, //上一曲【操做:按耳机线控中间按钮三下】 UIEventSubtypeRemoteControlPreviousTrack = 105, //快退开始【操做:按耳机线控中间按钮三下不要松开】 UIEventSubtypeRemoteControlBeginSeekingBackward = 106, //快退中止【操做:按耳机线控中间按钮三下到了快退的位置松开】 UIEventSubtypeRemoteControlEndSeekingBackward = 107, //快进开始【操做:按耳机线控中间按钮两下不要松开】 UIEventSubtypeRemoteControlBeginSeekingForward = 108, //快进中止【操做:按耳机线控中间按钮两下到了快进的位置松开】 UIEventSubtypeRemoteControlEndSeekingForward = 109, };
这里咱们将远程控制事件放到视图控制器(事实上不多直接添加到UI控件,通常就是添加到UIApplication或者UIViewController),模拟一个音乐播放器。
1.首先在应用程序启动后设置接收远程控制事件,而且设置音频会话保证后台运行能够播放(注意要在应用配置中设置容许多任务)
// // AppDelegate.m // TouchEventAndGesture // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "AppDelegate.h" #import "ViewController.h" #import <AVFoundation/AVFoundation.h> #import "KCApplication.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { _window=[[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds]; _window.backgroundColor =[UIColor colorWithRed:249/255.0 green:249/255.0 blue:249/255.0 alpha:1]; //设置全局导航条风格和颜色 [[UINavigationBar appearance] setBarTintColor:[UIColor colorWithRed:23/255.0 green:180/255.0 blue:237/255.0 alpha:1]]; [[UINavigationBar appearance] setBarStyle:UIBarStyleBlack]; ViewController *mainController=[[ViewController alloc]init]; _window.rootViewController=mainController; //设置播放会话,在后台能够继续播放(还须要设置程序容许后台运行模式) [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; if(![[AVAudioSession sharedInstance] setActive:YES error:nil]) { NSLog(@"Failed to set up a session."); } //启用远程控制事件接收 [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; // [self becomeFirstResponder]; [_window makeKeyAndVisible]; return YES; } //-(void)remoteControlReceivedWithEvent:(UIEvent *)event{ // NSLog(@"remote"); //} - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. } - (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil]; } - (void)applicationWillEnterForeground:(UIApplication *)application { // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. } - (void)applicationDidBecomeActive:(UIApplication *)application { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } - (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } @end
2.在视图控制器中添加远程控制事件并音频播放进行控制
// // ViewController.m // RemoteEvent // // Created by Kenshin Cui on 14-3-16. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "ViewController.h" @interface ViewController (){ UIButton *_playButton; BOOL _isPlaying; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self initLayout]; } -(BOOL)canBecomeFirstResponder{ return NO; } -(void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; _player = [[AVPlayer alloc] initWithURL:[NSURL URLWithString:@"http://stream.jewishmusicstream.com:8000"]]; //[_player play]; //_isPlaying=true; } #pragma mark 远程控制事件 -(void)remoteControlReceivedWithEvent:(UIEvent *)event{ NSLog(@"%i,%i",event.type,event.subtype); if(event.type==UIEventTypeRemoteControl){ switch (event.subtype) { case UIEventSubtypeRemoteControlPlay: [_player play]; _isPlaying=true; break; case UIEventSubtypeRemoteControlTogglePlayPause: if (_isPlaying) { [_player pause]; }else{ [_player play]; } _isPlaying=!_isPlaying; break; case UIEventSubtypeRemoteControlNextTrack: NSLog(@"Next..."); break; case UIEventSubtypeRemoteControlPreviousTrack: NSLog(@"Previous..."); break; case UIEventSubtypeRemoteControlBeginSeekingForward: NSLog(@"Begin seek forward..."); break; case UIEventSubtypeRemoteControlEndSeekingForward: NSLog(@"End seek forward..."); break; case UIEventSubtypeRemoteControlBeginSeekingBackward: NSLog(@"Begin seek backward..."); break; case UIEventSubtypeRemoteControlEndSeekingBackward: NSLog(@"End seek backward..."); break; default: break; } [self changeUIState]; } } #pragma mark 界面布局 -(void)initLayout{ //专辑封面 UIImage *image=[UIImage imageNamed:@"wxl.jpg"]; UIImageView *imageView=[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame]; imageView.image=image; imageView.contentMode=UIViewContentModeScaleAspectFill; [self.view addSubview:imageView]; //播放控制面板 UIView *view=[[UIView alloc]initWithFrame:CGRectMake(0, 480, 320, 88)]; view.backgroundColor=[UIColor lightGrayColor]; view.alpha=0.9; [self.view addSubview:view]; //添加播放按钮 _playButton=[UIButton buttonWithType:UIButtonTypeCustom]; _playButton.bounds=CGRectMake(0, 0, 50, 50); _playButton.center=CGPointMake(view.frame.size.width/2, view.frame.size.height/2); [self changeUIState]; [_playButton addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside]; [view addSubview:_playButton]; } #pragma mark 界面状态 -(void)changeUIState{ if(_isPlaying){ [_playButton setImage:[UIImage imageNamed:@"playing_btn_pause_n.png"] forState:UIControlStateNormal]; [_playButton setImage:[UIImage imageNamed:@"playing_btn_pause_h.png"] forState:UIControlStateHighlighted]; }else{ [_playButton setImage:[UIImage imageNamed:@"playing_btn_play_n.png"] forState:UIControlStateNormal]; [_playButton setImage:[UIImage imageNamed:@"playing_btn_play_h.png"] forState:UIControlStateHighlighted]; } } -(void)btnClick:(UIButton *)btn{ if (_isPlaying) { [_player pause]; }else{ [_player play]; } _isPlaying=!_isPlaying; [self changeUIState]; } @end
运行效果(真机截图):
注意: