免責申明(必讀!):本博客提供的所有教程的翻譯原稿均來自於互聯網,僅供學習交流之用,切勿進行商業傳播。同時,轉載時不要移除本申明。如產生任何糾紛,均與本博客所有人、發表該翻譯稿之人無任何關系。謝謝合作!
原文:http://www.raywenderlich.com/6567/uigesturerecognizer-tutorial-in-ios-5-pinches-pans-and-more
IOS 5手勢識別教程:二指撥動、拖移以及更多手勢
本教程Powered By Benna, review by iven、子龍山人。

如果在你的應用程序中需要檢測手勢,比如點擊(tap)、二指撥動 (pinch)、拖移(pan)和旋轉(rotation),那么通過創建UIGestureRecognizer類來實現將十分簡單。
在本教程中,我們將向你展示如何在你的應用程序里通過簡單地編程,添加手勢識別,同時在IOS 5中使用故事版(Storyboard)編輯器。
我們將創建一個簡單的應用程序,應用里你可以利用手勢識別器通過拖動、二指撥動、旋轉來移動一只猴子和香蕉。
我們還將展示如下一些很炫的東西:
- 添加減速運動
- 設置手勢識別器依賴
- 添加一個自定義的UIGestureRecognizer,這樣你就可以給猴子撓癢癢!J
這個教程需要你熟悉IOS 5中ARC和Storyboard的基本概念。如果你剛剛接觸這些概念,可以先閱讀我們ARC和Storyboard教程。
我想剛剛那只猴子給我們豎起了大拇指,那么現在我們開始吧!J
Getting Started
打開Xcode,建立一個新的項目,點擊File->new->new project,選擇IOS->Application下的Single View Application應用程序模版。將項目名為“MonkeyPinch”,Device Family選擇iPhone,將下面的“Use Storyboard”和“Use Automatic Reference Counting”的選擇框打上鈎。
首先,下載這個項目的資源並將這4份文件添加到你的項目中。如果你好奇這些,本教程的所有圖片均來自我可愛妻子的免費游戲素材包,我們還一起用麥克風制作了音效素材。:P
接下來,打開MainStoryboard.storyboard,拖一個Image View到View Controller。設置image為monkey_1.png,通過Editor->Size to Fit Content,調整Image View大小到圖片大小。接着拖第二個Image View進入View Controller,設置圖片為object_banababunch.png,同樣調整大小。在View Controller中隨意放置這兩個Image View,你將看到如下圖:

這是本應用的UI,現在讓我們添加手勢識別器來拖動這些image view!
UIGestureRecognizer 概述
在我們開始之前,讓我們給你一個簡要的概述來闡明怎么使用UIGestureRecognizers以及為什么它們非常好用。
在過去沒有UIGestureRecognizers的日子里,如果你想檢測一個手勢,例如滑動(swipe),你必須在一個UIView中對每個touch注冊notification,例如touchesBegan,touchesMoves和touchesEnded。每個程序員檢測touch的編寫稍微有點不同就會導致奇怪的bug和應用上的不一致。
在IOS 3.0,蘋果使用了新的UIGestureRecognizer類來解決這個問題!它提供了一個 基本手勢(點擊tap、二指撥動pinch,旋轉rotation,滑動swipe,拖移pan,長按long press)的默認實現。使用他們之后,不僅節省了你一大堆代碼,而且能使你的應用工作得更正確!
使用UIGestureRecognizers是非常簡單的。你只用完成如下的步驟:
- 創建手勢識別器。當你創建一個手勢識別器,你需要指定一條回調函數,這樣當發生手勢開始、變化、結束的時候,手勢識別器就能傳送你的更新。
- 將手勢識別器添加到視圖上。每個手勢識別器需要連接一個(並且只能一個)視圖。當一個touch發生於一個視圖邊界(bounds)內,這個手勢識別器將會檢查此touch事件是否符合該手勢預定義的類型,如果找到匹配的,它就會調用回調函數。
你可通過編程完成這兩步(我們將遲點在這個教程上說明),但是在Storyboard編輯器里可視化添加一個手勢識別器更為簡單。那么讓我們去看看它是如何工作的並在這個項目中添加我們第一個手勢識別器。
UIPanGestureRecognizer
還是打開MainStoryboard.storyboard,在Object Library里找到Pan Gesture Recognizer,並將它拖到猴子這個image view上。這樣就創建好了撥動手勢識別器,它鏈接了猴子image view。如果你想確認手勢識別器和視圖是否已連接,可以通過點擊猴子image view,查看Connections Inspector,確定Pan Gesture Recognizer在gestureRecognizers collection:

你可能奇怪為什么我們將識別器關聯在這個圖像視圖而不是整個視圖。兩種方式都可以,只是上述的方法更合適你的項目。當我們將識別器關聯到猴子,我們能知道任何在猴子邊界內的touch事件,這樣我們就能很好地處理。這個方法的缺點是有時候你可能會想使接觸擴大到邊界之外。這樣的情況下,你可以將手勢識別器添加到整個視圖上,但是你不得不寫更多代碼確認用戶接觸在猴子或者香蕉邊界內,然后做出相應的處理。
現在我們已經創建了pan(撥動)手勢識別器並將它關聯到image view,我們只需寫回調函數,這樣當二指撥動發生時,我們就能做一些實際的事情。
打開ViewCotroller.h,添加如下聲明:
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer;
接着在ViewController.m中實現如下:
View Code
1 - (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer { 2 CGPoint translation = [recognizer translationInView:self.view]; 3 recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x, recognizer.view.center.y + translation.y); 4 [recognizer setTranslation:CGPointMake(0, 0) inView:self.view]; 5 }
當pan手勢最先被檢測到,UIPanGestureRecognizer將會調用這個方法,接着當用戶繼續撥動,到最后一個撥動完成(通常是用戶抬起手指),這個方法會繼續。
UIPanGestureRecognizer將自己作為參數傳遞到這個方法。你可以檢索用戶移動手指調用translationInView這個方法的數量值。在這里我們用手指拖拽的數量值來移動猴子圖像中心。
注意一旦你完成上述的移動,將translation重置為0十分重要。否則translation每次都會疊加,很快你的猴子就會移除屏幕!
注意比起用硬編碼直接將猴子image view寫進這個方法里,在調用recognizer.view時我們得到一個猴子image view的關聯。這能使我們的代碼更能據普遍性,所以接下來香蕉image view我們也將 再次使用這種方法。
OK,現在這個方法已經完成,讓我們將它關聯到UIPanGestureRecognizer。在Interface Builder中選擇UIPanGestureRecognizer,打開Connections Inspector,從選擇器中拖一條線到View Controller。一個彈出框將會出現,選擇handlePan。這時,你Pan Gesture Recognizer的Connections Inspector應該如下圖:

編譯運行,試着拖動猴子。。等等,它還是不會動。
它不會動的原因是通常視圖不接受觸摸,默認touch無法使用,就像Image View。所以選擇所有的image view,打開Attributes Inspector,勾選“User Interaction Enabled”。

再次編譯運行,這次你就能在屏幕上拖動猴子了!

注意你不能拖動香蕉。這是因為手勢識別器被連接到一個(只能一個)視圖。所以繼續為香蕉添加另一個視圖,實現步驟如下:
- 拖一個Pan Gesture Recognizer到香蕉image view上。
- 選擇一個新的Pan Gesture Recognizer,選擇Connections Inspector,從選擇器中拖一條線到View Controller,將它連接到handlePan。
現在試試你應該可以在屏幕上拖動這兩個image view。相當簡單的實現就能得到這么酷和有意思的效果。
Gratuitous Deceleration
在大多數的蘋果應用和控件中,當你你停止移動某物后,在它結束移動時將有一點減速,例如滾動一個web view。在應用中想要這類型的行為是很普遍的。
有很多種方式能做到這樣,我們將以一種非常簡單的方式實現,這種方式看起來很粗糙但實際效果很漂亮。這個想法是當我們檢測到手勢結束的時候,計算touch移動的速度有多快,基於touch的速度來使這個物體向一個最后的終點做運動動畫。
- 檢測手勢結束:當手勢識別器改變狀態去開始、變化或者結束,傳遞到手勢識別器的回調可能被調用多次 。我們通過查看手勢識別器的狀態屬性就能很簡單地知道它是什么狀態。
- 檢測touch速度:一些手勢識別器返回額外的消息——你可以通過查看API向導知道你能獲得什么。在UIPanGestureRecognizer的使用中,有一個很便捷的方法叫velocityInView!
添加如下代碼到handlePan方法的末尾:
View Code
1 if (recognizer.state == UIGestureRecognizerStateEnded) { 2 3 CGPoint velocity = [recognizer velocityInView:self.view]; 4 5 CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y)); 6 7 CGFloat slideMult = magnitude / 200; 8 9 NSLog(@"magnitude: %f, slideMult: %f", magnitude, slideMult); 10 11 float slideFactor = 0.1 * slideMult; // Increase for more of a slide 12 13 CGPoint finalPoint = CGPointMake(recognizer.view.center.x + (velocity.x * slideFactor), 14 15 recognizer.view.center.y + (velocity.y * slideFactor)); 16 17 finalPoint.x = MIN(MAX(finalPoint.x, 0), self.view.bounds.size.width); 18 19 finalPoint.y = MIN(MAX(finalPoint.y, 0), self.view.bounds.size.height); 20 21 [UIView animateWithDuration:slideFactor*2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{ 22 recognizer.view.center = finalPoint; 23 24 } completion:nil]; 25 }
這是我為這個教程模擬減速寫的一個很簡單的方法。它遵循如下策略:
- 計算速度向量的長度(i.e. magnitude)
- 如果長度小於200,則減少基本速度,否則增加它。
- 基於速度和滑動因子計算終點
- 確定終點在視圖邊界內
- 讓視圖使用動畫到達最終的靜止點
- 使用“Ease out“動畫參數,使運動速度隨着時間降低
編譯並運行,現在你有一些基本但是漂亮的減速了!讓我們更加自由地玩,然后提高它!如果你想到更好的實現,請在本文的最后分享到論壇。
UIPinchGestureRecognizer 和 UIRotationGestureRecognizer
我們的應用到目前為止進展得不錯,但是如果你能縮放、旋轉image view,它能更加酷!
讓我們先添加回調函數,將如下聲明添加到ViewController.h中:
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer;
- (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer;
添加如下實現到ViewController.m:
View Code
1 - (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer { 2 3 recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale, recognizer.scale); 4 recognizer.scale = 1; 5 6 } 7 8 - (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer { 9 10 recognizer.view.transform = CGAffineTransformRotate(recognizer.view.transform, recognizer.rotation); 11 recognizer.rotation = 0; 12 13 }
就像我們能從pan手勢識別器獲得平移一樣,我們也能從UIPinchGestureRecognizer和UIRotationGestureRecognizer中獲得縮放和旋轉。
通過應用於視圖的旋轉、縮放和平移里的信息,每個視圖都會被應用一些變換。蘋果建立了大量的方法讓變換作用起來更為簡單,比如CGAffineTransformScale(給予縮放變換)和CGAffineTransformRotate(給予旋轉變換)。在這里我們僅僅基於手勢使用這些來更新視圖變換。
再次聲明,當每次手勢更新后我們在更新視圖時,重置縮放和旋轉為默認狀態非常重要,這樣我們才不至於在接下來發狂。
現在讓我們在Storyboard編輯器中建立關聯。打開MainStoryboard.storyboard並執行如下步驟:
- 拖一個Pinch Gesture Recognizer和Rotation Gesture Recognizer到猴子上,香蕉上也這么做。
- 將Pinch Gesture Recognizer的選擇器連接到View Controller的handlePinch方法。將Rotation Gesture Recognizer的選擇器連接到View Controller的handleRotate方法上。
編譯並運行(我建議條件允許的話,運行在真機設備上,因為二指撥動和旋轉在模擬器上有點難做),現在你應該能縮放和旋轉猴子和香蕉了!

Simultaneous Gesture Recognizers
你可能注意到如果你放一根手指在猴子上,放另一根在香蕉上,你能同時拖動他們,有點酷,是不是?
然而,你將會注意到如果你試着到處拖動猴子的中途,放下第二根手指企圖二指撥動來縮放猴子,這是不起作用的。默認情況下,一旦視圖上的一個手勢識別器“認領”了這個手勢,那之后其他手勢識別器就不能再識別這個手勢。
可是,你能通過覆蓋UIGestureRecognizer委托中的方法改變這種現狀。讓我們看看如何實現!
打開ViewController.h並標記這個類實現UIGestureRecognizerDelegate,顯示如下:
@interface ViewController : UIViewController <UIGestureRecognizerDelegate>
接着轉到ViewController.m並實現一個可選方法,你能覆蓋它:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
這個方法描述了當一個(給定的)手勢識別器已經檢測到了手勢,另一個手勢識別器再去識別這個手勢是否OK。
下一步,打開MainStoryboard.storyboard,將每個手勢識別器連接到視圖上自己的委托輸出口(outlet)。
再次編譯並運行這個應用,現在你應該能拖動這只猴子時,二指撥動縮放它,然后繼續拖動!你甚至能自然地同時縮放、旋轉,這樣能使用戶有更好的體驗。
Programmatic UIGestureRecognizers(實用的手勢識別器)
到目前為止,我們已經在Storyboard編輯器上創建了手勢識別器,但是如果你想用程序實現呢?
這也是很簡單的,所以讓我們添加一個點擊手指識別器,在點擊任何一個image view的時候播放音效。
由於將播放音效,所以我們需要添加 AVFoundation.framework到我們的項目。要做到這點,首先在項目導航中選擇你的項目,選擇MonkeyPinch target,選擇Build Phases選項卡,展開Link Binary with Libraries分區,點擊“添加”按鈕,選擇AVFoundation.framework。現在你的Framework列表應該看起來如下:

打開ViewCotroller.h,並做如下改變:
// Add to top of file
#import <AVFoundation/AVFoundation.h>
// Add after @interface
@property (strong) AVAudioPlayer * chompPlayer;
- (void)handleTap:(UITapGestureRecognizer *)recognizer;
添加如下修改到ViewController.m
ViewController.m
1 // After @implementation 2 @synthesize chompPlayer; 3 4 // Before viewDidLoad 5 - (AVAudioPlayer *)loadWav:(NSString *)filename { 6 7 NSURL * url = [[NSBundle mainBundle] URLForResource:filename withExtension:@"wav"]; 8 9 NSError * error; 10 11 AVAudioPlayer * player = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error]; 12 13 if (!player) { 14 NSLog(@"Error loading %@: %@", url, error.localizedDescription); 15 } else { 16 [player prepareToPlay]; 17 } 18 19 return player; 20 } 21 22 // Replace viewDidLoad with the following 23 - (void)viewDidLoad 24 { 25 26 [super viewDidLoad]; 27 28 for (UIView * view in self.view.subviews) { 29 30 UITapGestureRecognizer * recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; 31 recognizer.delegate = self; 32 [view addGestureRecognizer:recognizer]; 33 34 // TODO: Add a custom gesture recognizer too 35 } 36 self.chompPlayer = [self loadWav:@"chomp"]; 37 } 38 39 // Add to bottom of file 40 - (void)handleTap:(UITapGestureRecognizer *)recognizer { 41 42 [self.chompPlayer play]; 43 }
這個聲音播放代碼超出了本教程的范疇,所以我們將不討論它(雖然它也非常地簡單)。
ViewDidLoad的部分很重要。我們遍歷了所有的subview(只有猴子和香蕉 image view)並為每個子視圖添加了一個UITapGestureRecognizer,制定了回調函數。我們通過代碼設定了委托,將識別器添加到了視圖。
就是這樣!編譯並運行,現在你能在點擊image view的時候聽到音效!
UIGestureRecognizer Dependencies
應用運行得相當好,除了一個小小的瑕疵。如果你將一個物體拖動了一點點距離,它就將被拖移並播放音效,但是我們其實想要的只是播放音效而沒有拖移發生。
為了解決這個問題,我們可以移除或者修改委托回調在touch和pinch同時發生時做不一樣的行為。但是我想用這個例子來證明另一件很有用的事情——你能用手勢識別器這么做:設置相關性。
有一個叫做requireGestureRecognizerToFail方法,你可以在手勢識別器上調用。你猜猜它能做什么呢?;]
讓我們試試。打開MainStoryboard.storyboard,打開Assistant Editor,確定ViewController.h在這兒顯示。接着,將猴子的pan手勢識別器控件拖到@interface文件中,建立名為monkeyPan輸出口。同樣對香蕉的pan手勢識別器這么做,輸出口命名為bananaPan。
接着在viewDidLoad中簡單地添加兩行,在TODO之前比較正確:
[recognizer requireGestureRecognizerToFail:monkeyPan];
[recognizer requireGestureRecognizerToFail:bananaPan];
現在如果沒有pan被識別,點擊識別器才被調用。很酷,是不是?你可能會發現這項技術在你的項目中十分有用。
Custom UIGestureRecognizer
現在你已經知道了很多你需要知道的關於在你的應用中使用內部的手勢識別器的知識。但是如果你想檢測一些內部識別器不支持的手勢類型呢?
你當然可以寫你自己的手勢識別器!讓我們試着寫一個非常簡單的手勢識別器檢測你通過手指左右移動多次給猴子或者香蕉“撓癢癢”(tickle)。
建立新的文件,使用IOS\Cocoa Touch\Objective-C類模版,命名為TickleGestureRecognizer,使它繼承於UIGestureRecognizer。
接着根據如下替換TickleGestureRecognizer.h:
#import <UIKit/UIKit.h>
typedef enum {
DirectionUnknown = 0,
DirectionLeft,
DirectionRight
} Direction;
@interface TickleGestureRecognizer : UIGestureRecognizer
@property (assign) int tickleCount;
@property (assign) CGPoint curTickleStart;
@property (assign) Direction lastDirection;
@end
現在我們聲明了三個信息屬性,用於記錄檢測這個手勢。我們記錄:
- tickleCount:用戶改變了手指方向多少次(當移動最少數量的點)。一旦用戶移動手指方向3次,我們就當它是tickle手勢。
- curTickleStart:在tickle手勢中用戶開始移動的點。我們將每次更新用戶方向(當移動最少數量的點)。
- lastDirection:最后手指移動的方向。方向以unknown開始,在用戶移動最少數量的點之后我們將看看手指會向左或者像右,然后進行適當更新。
當然,這些屬性對於我們檢測的手勢是特定的——如果你為不同類型的手勢制作識別器,你將會有自己的屬性,但是你能在此獲得普遍的想法。
現在轉到TickleGestureRecognizer.m並替換為如下:
View Code
1 #import "TickleGestureRecognizer.h" 2 #import <UIKit/UIGestureRecognizerSubclass.h> 3 4 #define REQUIRED_TICKLES 2 5 #define MOVE_AMT_PER_TICKLE 25 6 7 @implementation TickleGestureRecognizer 8 9 @synthesize tickleCount; 10 @synthesize curTickleStart; 11 @synthesize lastDirection; 12 13 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 14 15 UITouch * touch = [touches anyObject]; 16 17 self.curTickleStart = [touch locationInView:self.view]; 18 } 19 20 - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 21 22 // Make sure we've moved a minimum amount since curTickleStart 23 UITouch * touch = [touches anyObject]; 24 25 CGPoint ticklePoint = [touch locationInView:self.view]; 26 27 CGFloat moveAmt = ticklePoint.x - curTickleStart.x; 28 29 Direction curDirection; 30 31 if (moveAmt < 0) { 32 curDirection = DirectionLeft; 33 } else { 34 curDirection = DirectionRight; 35 } 36 37 if (ABS(moveAmt) < MOVE_AMT_PER_TICKLE) return; 38 39 // Make sure we've switched directions 40 if (self.lastDirection == DirectionUnknown || 41 (self.lastDirection == DirectionLeft && curDirection == DirectionRight) || 42 (self.lastDirection == DirectionRight && curDirection == DirectionLeft)) { 43 44 // w00t we've got a tickle! 45 self.tickleCount++; 46 47 self.curTickleStart = ticklePoint; 48 49 self.lastDirection = curDirection; 50 51 // Once we have the required number of tickles, switch the state to ended. 52 // As a result of doing this, the callback will be called. 53 if (self.state == UIGestureRecognizerStatePossible && self.tickleCount > REQUIRED_TICKLES) { 54 55 [self setState:UIGestureRecognizerStateEnded]; 56 } 57 } 58 } 59 60 - (void)reset { 61 62 self.tickleCount = 0; 63 self.curTickleStart = CGPointZero; 64 self.lastDirection = DirectionUnknown; 65 66 if (self.state == UIGestureRecognizerStatePossible) { 67 68 [self setState:UIGestureRecognizerStateFailed]; 69 } 70 } 71 72 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event 73 { 74 [self reset]; 75 } 76 77 - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event 78 { 79 [self reset]; 80 } 81 82 @end
這兒的代碼比較多,但是為不准備重溫這些細節,因為坦白地說它們並不相當重要。最重要的部分是它怎么運作的普遍想法:我們實現了touchesBegan,touchesMoved,touchesEnded和touchesCancelled,寫自定義代碼查看這些touch和檢測我們的手勢。
一旦我們發現這個手勢,我們想傳送更新到回調方法。你可以通過轉換手勢識別器的狀態來做到。通常一旦一個手勢開始,你想要設置狀態為UIGestureRecognizerStateBegin,傳送任意更新時為UIGestureRecognizerStateChanged,完成時為UIGestureRecognizerStateEnded。
但是對這個簡單的手勢識別器而言,一旦用戶對某個物體撓癢癢,我們就標注它結束。這個回調將會被調用,將在那兒編寫實現代碼。
好了,現在使用新的手勢識別器。打開ViewController.h並做如下更改:
// Add to top of file
#import "TickleGestureRecognizer.h"
// Add after @interface
@property (strong) AVAudioPlayer * hehePlayer;
- (void)handleTickle:(TickleGestureRecognizer *)recognizer;
接着到ViewController.m中:
View Code
1 // After @implementation 2 @synthesize hehePlayer; 3 4 // In viewDidLoad, right after TODO 5 TickleGestureRecognizer * recognizer2 = [[TickleGestureRecognizer alloc] initWithTarget:self action:@selector(handleTickle:)]; 6 7 recognizer2.delegate = self; 8 9 [view addGestureRecognizer:recognizer2]; 10 11 // At end of viewDidLoad 12 self.hehePlayer = [self loadWav:@"hehehe1"]; 13 14 // Add at beginning of handlePan (gotta turn off pan to recognize tickles) 15 return; 16 17 // At end of file 18 - (void)handleTickle:(TickleGestureRecognizer *)recognizer { 19 [self.hehePlayer play]; 20 }
這樣你能看到這個自定義手勢識別器就像內部手勢識別器一樣簡單!
編譯並運行,就能聽到“he he, that tickles”。
何去何從?
這兒是項目源碼,含有所有本教程的代碼。
祝賀你,你現在有很多關於手勢識別器的經驗了!我希望你能在你的應用使用它們並享受它們!
