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。然后拖第二张图片进去并重设大小。

Pinches,Pans,and More!_第1张图片
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。

Pinches,Pans,and More!_第2张图片
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。
这时候,你的链接检查器看起来像这样的:

Pinches,Pans,and More!_第3张图片
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 

切换到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标签,把库添加进去。

Pinches,Pans,and More!_第4张图片
2D624622-46ED-42BD-A618-F157D206D896.png

打开ViewControl.h做如下改变:

// Add to top of file
#import  
// 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建立属性。

Pinches,Pans,and More!_第5张图片
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  
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 
#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];
}

现在你就可以使用自定义的手势了~

源码地址

你可能感兴趣的:(Pinches,Pans,and More!)