iPhone的成功很大一部分得益於它多點觸摸的強大功能,喬布斯讓人們認識到手機實際上是能夠不用按鍵和手寫筆直接操做的,這不愧爲一項偉大的設計。今天咱們就針對iOS的觸摸事件(手勢操做)、運動事件、遠程控制事件等展開學習:html
在iOS中事件分爲三類:ios
下圖是蘋果官方對於這三種事件的形象描述:算法
在iOS中並非全部的類都能處理接收並事件,只有繼承自UIResponder類的對象才能處理事件(如咱們經常使用的UIView、UIViewController、UIApplication都繼承自UIResponder,它們都能接收並處理事件)。在UIResponder中定義了上面三類事件相關的處理方法:數組
事件 | 說明 |
---|---|
觸摸事件 | |
- (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中是最經常使用的事件,這裏咱們首先介紹觸摸事件。session
在下面的例子中定義一個KCImage,它繼承於UIView,在KCImage中指定一個圖片做爲背景。定義一個視圖控制器KCTouchEventViewController,而且在其中聲明一個KCImage變量,添加到視圖控制器中。既然UIView和UIViewController都繼承於UIResponder,那麼也就就意味着全部的UIKit控件和視圖控制器均能接收觸摸事件。首先咱們在KCTouchEventViewController中添加觸摸事件,並利用觸摸移動事件來移動KCImage,具體代碼以下:app
// // 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
如今運行程序:dom
上面示例中咱們用到了UITouch類,當執行觸摸事件時會將這個對象傳入。在這個對象中包含了觸摸的全部信息:ide
從上面運行效果能夠看到不管是選擇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
運行效果(真機截圖):
注意: