IOS 5手势识别教程:二指拨动、拖移以及更多手势
免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!
原文:http://www.raywenderlich.com/6567/uigesturerecognizer-tutorial-in-ios-5-pinches-pans-and-more
IOS 5手势识别教程:二指拨动、拖移以及更多手势
Made in iTyran,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中实现如下:
- (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手势最先被检测到,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方法的末尾:
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]; }
这是我为这个教程模拟减速写的一个很简单的方法。它遵循如下策略:
- 计算速度向量的长度(i.e. magnitude)
- 如果长度小于200,则减少基本速度,否则增加它。
- 基于速度和滑动因子计算终点
- 确定终点在视图边界内
- 让视图使用动画到达最终的静止点
- 使用“Ease out“动画参数,使运动速度随着时间降低
编译并运行,现在你有一些基本但是漂亮的减速了!让我们更加自由地玩,然后提高它!如果你想到更好的实现,请在本文的最后分享到论坛。
UIPinchGestureRecognizer 和 UIRotationGestureRecognizer
我们的应用到目前为止进展得不错,但是如果你能缩放、旋转image view,它能更加酷!
让我们先添加回调函数,将如下声明添加到ViewController.h中:
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer; - (IBAction)handleRotate:(UIRotationGestureRecognizer *)recognizer;
添加如下实现到ViewController.m:
- (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手势识别器获得平移一样,我们也能从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
// After @implementation @synthesize chompPlayer; // Before viewDidLoad - (AVAudioPlayer *)loadWav:(NSString *)filename { NSURL * url = [[NSBundle mainBundle] URLForResource:filename withExtension:@"wav"]; NSError * error; AVAudioPlayer * player = [[AVAudioPlayer alloc] initWithContentsOfURL: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"]; } // Add to bottom of file - (void)handleTap:(UITapGestureRecognizer *)recognizer { [self.chompPlayer play]; }
这个声音播放代码超出了本教程的范畴,所以我们将不讨论它(虽然它也非常地简单)。
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并替换为如下:
#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:(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)reset { self.tickleCount = 0; self.curTickleStart = CGPointZero; self.lastDirection = DirectionUnknown; if (self.state == UIGestureRecognizerStatePossible) { [self setState:UIGestureRecognizerStateFailed]; } } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [self reset]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { [self reset]; } @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中:
// 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]; }
这样你能看到这个自定义手势识别器就像内部手势识别器一样简单!
编译并运行,就能听到“he he, that tickles”。
何去何从?
这儿是项目源码,含有所有本教程的代码。
祝贺你,你现在有很多关于手势识别器的经验了!我希望你能在你的应用使用它们并享受它们!
额外福利:本教程pdf下载
想和更多人交流,请猛击:传送门!