美文网首页
Pinches,Pans,and More!

Pinches,Pans,and More!

作者: 董二千 | 来源:发表于2016-03-21 00:19 被阅读123次

    如果你要在你的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];
    }
    

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

    源码地址

    相关文章

      网友评论

          本文标题:Pinches,Pans,and More!

          本文链接:https://www.haomeiwen.com/subject/xqthlttx.html