如果你要在你的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。然后拖第二張圖片進去并重設大小。
現在讓我們添加一個手勢,這樣我們就能四處移動我們的圖片了。
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。
現在我們已經創建了拖拽手勢,并把它和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。
這時候,你的鏈接檢查器看起來像這樣的:
注意,現在你不能拖拽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標簽,把庫添加進去。
打開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建立屬性。
添加下面的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];
}
現在你就可以使用自定義的手勢了~