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阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容