Pinches,Pans,and More!

如果你要在你的app中檢測手勢,像,點擊、捏合、拖拽或旋轉。這些都是很簡單的,通過類UIGestureRecognizer來創建。在這里你將會學到怎么在你app中添加手勢,通過Storyboard和代碼兩種方法。
我們將建一個簡單的app,你可以通過移動一個猴子,拖拽、捏合、旋轉一個??。這些都將通過手勢來實現。

知識點:
1.兩個手勢并存的情況。
2.實現慣性的減速。
3.一個手勢要在另一個手勢失敗了才發生。
4.自定義一種手勢比如:撓癢

Starting

打開XCode創建一個新項目(iOS/Application/Single View)。項目名稱為MonkeyPinch,設備旋轉iPhone,并且選擇Storyboard和ARC。
然后打開MainStoryboard.storyboard,把圖片拖到View Controller。把image設置為monkey_1.png,并且重新設置Image view的大小,通過Editor\Size to Fit Content。然后拖第二張圖片進去并重設大小。

Screen Shot PM.png

現在讓我們添加一個手勢,這樣我們就能四處移動我們的圖片了。

UIGestureRecognizer 簡介

在我們開始之前,我們對怎么使用UIGestureRecognizer和為什么使用它是方便的做一個概述。

在UIGestureRecognizer出現之前,如果你想要檢測一個手勢例如swipe,你必須要在每一個UIView內為每個touch注冊一個通知,例如touchesBegan,touchesMoves和touchesEnded。每個檢測手勢的code只有一點點細微的不同,容易引起一些細微的bug和沖突。

在iOS3.0 蘋果為UIGestureRecognizer類增加了新的API,這些API提供了檢測普通手勢的默認實現,像,pinches、taps、rotations、swipes、pans、long press。通過使用它們,不需要保存大量的code,就能讓你的app運行的很好。

使用UIGestureRecognizer是非常簡單的。你只要完成接下來的幾步。

  • 創建一個手勢。當你創建一個手勢你需要實現一個回調方法。當手勢開始,變換和結束的時候,通知你。
  • 添加一個手勢到view上面。每一個手勢和一個view相關聯。當touch發生在view的bounds范圍內,gesture recognizer將會識別,是否該手勢匹配它尋找的touch類型,如果找到它,就會觸發回調。

你可以用代碼完成這兩步,但是在Storyboard上面完成這些操作更加的簡單。讓我們看看它怎么工作的,并添加第一個手勢到我們的項目中。

UIPanGestureRecognizer

打開Storyboard,把 Pan Gesture Recognizer 拖拽到 monkey Image View上面。這一步同時完成了兩步,創建了一個手勢,把手勢和monkey Image View鏈接在一起。你可以點擊monkey Image View,查看連接器,來驗證鏈接OK。確保 Pan Gesture Recognizer在手勢的集合中。注意將Image View屬性檢查器中的User Interaction Enabled 設置為YES,默認為NO。

Screen Shot.png

現在我們已經創建了拖拽手勢,并把它和image view關聯,我們必須要寫我們的回調方法。這樣我們就能在pan發生的時候做一些事情。

打開ViewController.h添加下面的聲明

- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer;

在ViewControl.m中實現它

- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:self.view]; 
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x, recognizer.view.center.y + translation.y); 
[recognizer setTranslation:CGPointMake(0, 0) inView:self.view]; 
}

當pan gesture 第一次被檢測到的時候,UIPanGestureRecognizer將會調動這個方法,當用戶繼續拖動的時候繼續檢測,最后一次是手勢完成的時候(通常是用戶手指離開屏幕)。

在這個方法里UIPanGestureRecognizer把自己作為參數。通過調用translationInView這個方法可以查看用戶移動手指產生的結果。我們通過這個值來移動monkey的center,它和手指移動的距離是一樣的。

注意,每一次設置你的translation為0是極其重要的,否則translation將會被混合(這一次和上一次),你會發現你的monkey迅速的被移除屏幕。

注意,除了硬編碼image view到這個方法里,我們通過調用recognizer.view獲取一個image view的引用。這是我們的code更加的泛型,所以稍后我們可以重用這個方法在banana image上。

現在這個方法完成了,讓我們把它和UIPanGestureRecognizer鏈接起來。選擇interface Builder里面的UIPanGeRecognizer,打開connections inspector,從方法上面拉一根線到viewcontroller。一個彈框就出現啦,選擇 handlePan。
這時候,你的鏈接檢查器看起來像這樣的:

C41341D8-B516-46A9-8F1A-AADA4555BA28.png

注意,現在你不能拖拽banana。這是因為,gesture recognizer只捆綁了一個view。所以去為banana添加一個手勢吧。

減速問題

在許多蘋果的app里,當你停止移動某物的時候,會有一個短暫的減速直到停止,例如滑動一個web view。在app里面實現這種行為是很常見的。

有很多辦法來實現它,但是我們打算用一種簡單粗糙的實現,效果也不差哦。想法是,當手勢結束的時候檢測它,計算出移動的速度。基于觸摸移動的速度,是這個對象最終移動到目的地。

  • 手勢結束的時候檢測。手勢的回調被調用多次,當gesture recognizer的狀態 從begin,到changed,再到ended。我們可以通過看recognizer的state屬性看它的狀態。
  • 檢測觸摸的速度。gesture recognizer還會返回一些其他的信息-你能通過API查看他們。velocityInView是一個很方便的方法在使用UIPanGestureRecognizer。

所以,在handlePan方法后面添加下面的代碼。

if (recognizer.state == UIGestureRecognizerStateEnded)
{ 
CGPoint velocity = [recognizer velocityInView:self.view]; 
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y)); 
CGFloat slideMult = magnitude / 200;
 NSLog(@"magnitude: %f, slideMult: %f", magnitude, slideMult);  
float slideFactor = 0.1 * slideMult; 
// Increase for more of a slide 
CGPoint finalPoint = CGPointMake(recognizer.view.center.x + (velocity.x * slideFactor), recognizer.view.center.y + (velocity.y * slideFactor)); 
finalPoint.x = MIN(MAX(finalPoint.x, 0), self.view.bounds.size.width); 
finalPoint.y = MIN(MAX(finalPoint.y, 0), self.view.bounds.size.height);  
[UIView animateWithDuration:slideFactor*2 delay:0 options:UIViewAnimationOptionCurveEaseOut 
animations:^{
 recognizer.view.center = finalPoint; 
} 
completion:nil];
}

這是一個非常簡單的方法,我寫上來為了模擬減速效果。它采取了下面的方法。

  • 計算出速度矢量
  • 如果值小于200,減速,否則加速
  • 基于速度和滑動因素計算出最終的點
  • 確保最終的落點在view的bounds內
  • 使用動畫
  • 動畫的時候使用option的ease out選項,使它緩慢的減速

UIPinchGestureRecognizer和UIRotationGestureRecognizer

我們的app到目前為止已經變得越來越棒了,如果你通過捏合和旋轉手勢來縮放和旋轉它,它將變的更加的酷!

添加下面的code到ViewController.h文件里

- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer;
- (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer;

添加下面的code到實現文件里

- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer {
 recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale, recognizer.scale); recognizer.scale = 1; 
}
 - (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer { 
recognizer.view.transform = CGAffineTransformRotate(recognizer.view.transform, recognizer.rotation); recognizer.rotation = 0;
 }

就像上面,我們可以從pan gesture recotnizer拿到translation一樣,我們可以從UIPinchGestureRecognizer和UIRotationGestureRecognizer里拿到scale和rotation。

每個view上面都被賦予以一種轉換,正如你所想到的旋轉、縮放等。蘋果為它定義了很多簡單的方法。像CGAffineTransformScale和CGAffineTransformRotate。這里我們僅僅使用基于手勢的視圖的transfrom更新。

現在讓我們把這些方法和storyboard編輯器鏈接起來。打開storyboard執行下面的步驟。

  • 拖一個Pinch Gesture Recognizer和Rotation Gesture Recognizer到monkey上面。banana也一樣。
  • 把手勢的方法和view controller 里面的方法鏈接起來。

手勢沖突

你可能會注意到,如果你放一個手指在monkey上,另一個放在banana上。你可以同時拖動它們,有點酷,是嗎。
但是,你將會注意到,如果你嘗試在拖動一個Monkey的同時放下第二根手指來嘗試縮放它,它不起作用了。默認情況下,一旦一個gesture recognizer被一個view所識別,這個view就不能對其他gesture recognizer識別。
但是你可以改變這種情況,通過覆寫UIGestureRecognizer Delegate里的一個方法,下面讓我們看看它是怎么工作的。

打開ViewController.h文件,使這個類遵守UIGestureRecognizerDelegate這個協議

@interface ViewController : UIViewController <UIGestureRecognizerDelegate>

切換到ViewControl.m 文件,實現你要覆寫的一個可選方法

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { 
    return YES;
}

這個方法告訴手勢識別器,這個允許的,當另一個手勢被檢測到的時候。也就是兩個手勢并存的情況。默認是NO。
下面打開MainStoryboard.storyboard,把ViewControl設為每個手勢的代理者,編譯允許你的app,that's great!

用代碼來實現UIGestureRecognizers

到目前為止我們都是通過Storyboard的編輯器來創建手勢的,但是如果你想要通過code來創建,怎么操作呢?
這很簡單,讓我們來嘗試它。添加一個點擊手勢,兩者中的任意一張圖片被點擊的時候,會產生一個播放音樂的效果。

由于我們要播放一段音樂,我們需要添加一個AVFoundation.framework到你的項目中。在Project navigator中選中你的project,選擇MonkeyPinch target,選擇Build Phase標簽,把庫添加進去。

2D624622-46ED-42BD-A618-F157D206D896.png

打開ViewControl.h做如下改變:

// Add to top of file
#import <AVFoundation/AVFoundation.h> 
// Add after 
@interface@property (strong) AVAudioPlayer * chompPlayer;
- (void)handleTap:(UITapGestureRecognizer *)recognizer;

切換到ViewControl.m文件里面

// After @implementation
@synthesize chompPlayer;
 // Before viewDidLoad
- (AVAudioPlayer *)loadWav:(NSString *)filename {
   NSURL *url = [[NSBundle mainBundle] URLForResource:filename withExtension:@"wav"];
   NSError *error;
AVAudioPlayer = *player = [[AVAudioPlayer allow] initWithContentURL:url error:&error]
   if (!player) 
   {
        NSLog(@"Error loading %@: %@", url,    error.localizedDescription); 
   } else { 
       [player prepareToPlay]; 
            } 
return player;;
}
// Replace viewDidLoad with the following
- (void)viewDidLoad{
  [super viewDidLoad];
  for (UIView * view in self.view.subviews) { 
 UITapGestureRecognizer * recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]; 
recognizer.delegate = self;
  [view addGestureRecognizer:recognizer];  
// TODO: Add a custom gesture recognizer too  
  }  
    self.chompPlayer = [self loadWav:@"chomp"];
}

音樂播放超出了本教程的方法(其實難以置信的簡單哦)。

手勢依賴

Project工作的很好除了,有一點點瑕疵。就是當你輕輕拖動的時候,它也播放音樂,但是這不是 我們希望看到的。
為了解決這個問題,我們應該移除或者監聽手勢的回調。對不同的手勢進行不同的處理。但是我想通過這種情況來證明另外一個有用的知識點:通過設置手勢依賴,對手勢進行處理。

這個方法叫做requireGestureRecognizerToFail
讓我們來嘗試一下。打開MainStoryboard.storyboard,打開Assistant Editor,確保ViewController.h出現在右邊。
通過control-drag 為monkey和banana建立屬性。

15726BF1-CDE1-4D41-BF61-1A959019E045.png

添加下面的code到viewDidLoad里面

[recognizer requireGestureRecognizerToFail:monkeyPan];[recognizer requireGestureRecognizerToFail:bananaPan];

這樣只有在拖拽手勢失敗的時候,點擊手勢才生效。

自定義手勢

到這里你已經收獲了很多關于手勢的知識,但是你還應該學會自定義手勢在你的app中。
讓我們來嘗試寫一個非常簡單的手勢。多次從左到右的移動你的手指多次,來為monkey或者banana撓癢。
創建一個新的文件,iOS\Cocoa Touch\Objective-C class,命名為TickleGestureRecognizer,它的超類是UIGestureRecognizer。

#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次,我們就認為手勢可以觸發了。
  • curTickleStart:用戶開始撓癢的這個點。用戶切換移動方向的時候我們每次都會更新這個點。
  • lastDirection:手指移動的最終方向。

當然這些屬性對我們要檢測的這個手勢來說是特殊的。
現在切換到TickleGestureRecognizer.m,用下面的code代替:

#import "TickleGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
#define REQUIRED_TICKLES 2
#define MOVE_AMT_PER_TICKLE 25 
@implementation TickleGestureRecognizer
@synthesize tickleCount;
@synthesize curTickleStart;
@synthesize lastDirection; 
- (void)touchesBegan:(
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 
   UITouch * touch = [touches anyObject]; 
   self.curTickleStart = [touch locationInView:self.view];
} 
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { 
 // Make sure we've moved a minimum amount since curTickleStart 
   UITouch * touch = [touches anyObject]; 
   CGPoint ticklePoint = [touch locationInView:self.view]; 
   CGFloat moveAmt = ticklePoint.x - curTickleStart.x; 
   Direction curDirection; 
   if (moveAmt < 0) { 
   curDirection = DirectionLeft; 
   } else { 
   curDirection = DirectionRight; 
    } 
    if (ABS(moveAmt) < MOVE_AMT_PER_TICKLE) return;  
// Make sure we've switched directions 
   if (self.lastDirection == DirectionUnknown || (self.lastDirection == DirectionLeft && curDirection == DirectionRight) || (self.lastDirection == DirectionRight && curDirection == DirectionLeft))
 {  
// w00t we've got a tickle! 
     self.tickleCount++; 
     self.curTickleStart = ticklePoint; 
     self.lastDirection = curDirection;   
// Once we have the required number of tickles, switch the state to ended. 
// As a result of doing this, the callback will be called.
     if (self.state == UIGestureRecognizerStatePossible && self.tickleCount > REQUIRED_TICKLES) { 
     [self setState:UIGestureRecognizerStateEnded]; 
        } 
    } 
} 
- (void)resetState { 
     self.tickleCount = 0; 
     self.curTickleStart = CGPointZero; 
     self.lastDirection = DirectionUnknown; 
    if (self.state == UIGestureRecognizerStatePossible) { 
     [self setState:UIGestureRecognizerStateFailed]; 
  }
}
 - (void)touchesEnded:([NSSet]  *)touches withEvent:(UIEvent *)event{
    [self resetState];
} 
- (void)touchesCancelled:([NSSet] *)touches withEvent:(UIEvent *)event{ 
   [self resetState];
}

 @end

代碼就是這些,但是我不打算詳細的去講這些,因為坦白的講,它們不是很重要。重要的是這個想法是如何工作的:我們實現了touchesBegan,touchesMoved, touchesEnded, and touchesCancelled方法并且自定義了code來檢測手勢,觀察touches。

一旦我們發現手勢,我們就想去通過回調來更新。你是通過切換gesture recognizer的state來達到這個目的的.通常只要手勢開始,你想要把狀態設為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

// After @implementation
@synthesize hehePlayer; 
// In viewDidLoad, right after TODO
TickleGestureRecognizer * recognizer2 = [[TickleGestureRecognizer alloc] initWithTarget:self action:@selector(handleTickle:)];
recognizer2.delegate = self;
[view addGestureRecognizer:recognizer2]; 
// At end of viewDidLoad
self.hehePlayer = [self loadWav:@"hehehe1"]; 
// Add at beginning of handlePan (gotta turn off pan to recognize tickles)
return;
 // At end of file
- (void)handleTickle:(TickleGestureRecognizer *)recognizer {
 [self.hehePlayer play];
}

現在你就可以使用自定義的手勢了~

源碼地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容