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。然后拖第二张图片进去并重设大小。
现在让我们添加一个手势,这样我们就能四处移动我们的图片了。
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。
这时候,你的链接检查器看起来像这样的:
注意,现在你不能拖拽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];
}
现在你就可以使用自定义的手势了~