程序员

iOS中手势的应用

iOS设备现如今大受欢迎的最重要原因之一就在于其开创了触控操作的潮流。发展到现在,无论是Android还是iPhone,现在APP与用户进行交互,基本上都是依赖于各种各样的触控事件。例如用户对屏幕进行了侧滑,APP就需要对这个手势进行相应的处理,给用户一个反馈。这些相应的事件就都是在UIResponder中定义的。

广告插播的措不及防:如果您要是觉得这篇文章让您有点收获,随手点个赞会让俺兴奋好久吶。

UIResponder大体有四类事件:触摸、加速计、远程控制、按压(iOS9.0以后出来的,3DTouch)。

触摸事件.png

但是在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件。我们称之为“响应者对象”。

UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件。

  • UIView继承自UIResponder,因此所有的控件都是响应者对象
  • UIWindow:是特殊的UIView,所以也是响应者对象
  • UIApplication,所以也是响应者对象

1. 四类事件的主要方法

有的童鞋可能分不清楚手势当中结束和取消的区别。举个栗子,当正在抚摸自己的爱机屏幕的时候,突然来了一个电话,这个“爱抚”的动作就被临时中断了,这个时候就叫做“取消”,而不是结束。

1.1 触摸事件

触摸事件分成了四部分:开始、移动、结束、取消。

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

1.2 加速计事件

加速计事件分成了三部分:开始、结束、取消。也有人叫做运动事件,motion events。

-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
-(void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

1.3 远程控制事件

收到控制事件:

-(void)remoteControlReceivedWithEvent:(UIEvent *)event;

1.4 按压事件

按压事件分成四部分:按压开始、按压改变、按压结束、按压取消。

-(void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

-(void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

-(void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

-(void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

2. 响应者链

概念吶,我们就不说了,网上的文章应该一搜一大堆。这里是比较理论化的知识,是比较考验我们对于iOS中触摸事件的理解深度的。这里我就只是用简单的方式,写一下自己对于这部分的理解。

根据第一部分的内容,知道UIResponder有好多好多。用户点击屏幕之后,系统到底让谁来响应这个触摸事件吶?例如用户点了一个button,是应该让谁来处理呢?
UIButton肯定是放在一个UIView上面,UIView也肯定是放在一个Controller里面。这几个都是响应者对象,总不能让大家一起给用户反馈吧。系统这里的原则其实是越具体越好,也就是干活的肯定是最小的那个小兵兵。要是什么事情都让UIApplication或者UIWindow干,还不让它两儿累死啊,那系统效率要低成神马样子。

最终找到这个干活的控件,我们学术上就叫做第一响应者对象。找到了负责处理的按钮之后如何给出相应处理呢?大概过程就是这样:

  • button尝试处理事件。如果它不能处理事件,则将事件传递给其父视图。
  • button的父视图(superview)尝试处理事件。如果这个父视图还不能处理事件,则继续将视图继续往上级传。
  • 上层视图(topmost view)会尝试处理事件。如果这个上层视图还是不能处理事件,则将事件传递给视图所在的视图控制器。
  • 视图控制器会尝试处理事件。如果这个视图控制器不能处理事件,则将事件传递给窗口(window)对象。
  • 窗口(window)对象尝试处理事件。如果不能处理,则将事件传递给UIApplication。
  • 如果UIApplication不能处理事件,则丢弃这个事件。就是白按喽。

    一次完整的触摸事件的传递响应的过程大概是这样的: UIAppliction –> UIWindow –>递归找到最适合处理事件的控件–>控件调用touches方法–>判断是否实现touches方法–>没有实现默认会将事件传递给上一个响应者–>找到上一个响应者。

对于第一响应者,UIResponder提供了一系列方法,我们分别来介绍一下。

  1. 如果想判定一个响应对象是否是第一响应者,则可以使用以下方法:
    • (BOOL)isFirstResponder
  2. 如果我们希望将一个响应对象作为第一响应者,则可以使用以下方法:
    • (BOOL)becomeFirstResponder
  3. 一个响应对象只有在当前响应者能放弃第一响应者状态(canResignFirstResponder)且自身能成为第一响应者(canBecomeFirstResponder)时才会成为第一响应者。换种说法就是,有人想当部门老大,那也得现在的部门头愿意放权,并且这个人还有能力才能成为老大。
//判断是否能够成为第一响应者
- (BOOL)canBecomeFirstResponder

//响应者放弃第一响应者身份
- (BOOL)resignFirstResponder
- (BOOL)canResignFirstResponder

这些方法大家用的都会比较多,特别是想让文本输入框获取到焦点的时候。

3. 手势识别功能(Gesture Recognizer)

  • 如果想监听一个view上面的触摸事件,之前的做法是
    • 自定义一个view
    • 实现view的touches方法,在方法内部实现具体处理代码
  • 通过touches方法监听view触摸事件,有很明显的几个缺点
    • 必须得自定义view
    • 由于是在view内部的touches方法中监听触摸事件,因此默认情况下,无法让其他外界对象监听view的触摸事件
    • 不容易区分用户的具体手势行为- iOS 3.2之后,苹果推出了手势识别功能(Gesture Recognizer),在触摸事件处理方面,大大简化了开发者的开发难度

3.1手势识别器(UIGestureRecognizer)

  • 为了完成手势识别,必须借助于手势识别器——UIGestureRecognizer
  • 利用UIGestureRecognizer,能轻松识别用户在某个view上面做的一些常见手势
  • UIGestureRecognizer是一个抽象类,定义了所有手势的基本行为,使用它的子类才能处理具体的手势
    • UITapGestureRecognizer(点按)
    • UIPinchGestureRecognizer(捏合,用于缩放)
    • UIPanGestureRecognizer(拖动)
    • UISwipeGestureRecognizer(轻扫)
    • UIRotationGestureRecognizer(旋转)
    • UILongPressGestureRecognizer(长按)

3.2 手势识别的使用方法

  • 1.创建手势识别实例
  • 2.设置手势识别属性,例如手指数量,方向等
  • 3.将手势识别附加到指定的视图之上
  • 4.编写手势触发监听方法
  • 每一个手势识别器的用法都差不多,比如UITapGestureRecognizer的使用步骤如下:
//创建手势识别器对象
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] init];

//设置手势识别器对象的具体属性,例如连续敲击2次
tap.numberOfTapsRequired = 2;
// 需要2根手指一起敲击
tap.numberOfTouchesRequired = 2;

//添加手势识别器到对应的view上
[self.iconView addGestureRecognizer:tap];

//监听手势的触发
[tap addTarget:self action:@selector(tapIconView:)];

3.3手势识别的枚举

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    // 没有触摸事件发生,所有手势识别的默认状态
    UIGestureRecognizerStatePossible,
    // 一个手势已经开始但尚未改变或者完成时
    UIGestureRecognizerStateBegan, (类似于 touchesBegan)
    // 手势状态改变
    UIGestureRecognizerStateChanged, (类似于 touchesMoved)
    // 手势完成
    UIGestureRecognizerStateEnded, (类似于 touchesEnded)
    // 手势取消,恢复至Possible状态
    UIGestureRecognizerStateCancelled, (比如手指按下按钮,然后从其他地方抬起)
    // 手势失败,恢复至Possible状态
    UIGestureRecognizerStateFailed,
    // 识别到手势识别
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

4. 手势的使用

4.1 长按手势

  • 长按手势一定要判断状态,否则方法会在手势开始和结束时分别调用!方法会被调用两次!
- (void)addLongPressGesture
{
    //创建长按手势识别并添加监听事件
    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
    //    给图片添加长按手势
    [self.imageView addGestureRecognizer:longPress];
}

//识别到长按手势后回调的方法
- (void)longPress:(UILongPressGestureRecognizer *)recognizer
{
    // 判断手势的状态,长按手势一定要判断状态,否则方法会在手势开始和结束时分别调用!方法会被调用两次!
    if (recognizer.state == UIGestureRecognizerStateBegan) {
        //动画  改变图片的透明度
        [UIView animateWithDuration:0.5 animations:^{
            self.imageView.alpha = 0.5;
        } completion:^(BOOL finished) {
            [UIView animateWithDuration:0.5 animations:^{
                self.imageView.alpha = 1.0;
            }];
        }];
    }
}

4.2 清扫手势

  • 如果要监听多个轻扫方向,需要添加多个轻扫手势
  • 轻扫手势默认支持向右的扫动方向
  • 因为轻扫手势要求用户比较放松的扫动,因此最好不要将此手势添加到某一个视图上,会局限用户的操作
- (void)addSwipeGesture
{
    // 如果要监听多个轻扫方向,需要添加多个轻扫手势
    // 轻扫手势默认支持向右的扫动方向

    //创建轻扫手势识别并添加监听事件(默认是向右扫动)
    UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];

    //创建轻扫手势识别并添加监听事件
    UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
    //direction 方向  向左轻扫
    swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft;

    /**
     UISwipeGestureRecognizerDirectionRight  向右轻扫(默认)不设轻扫方向 就是向右
     UISwipeGestureRecognizerDirectionLeft  向左轻扫
     UISwipeGestureRecognizerDirectionUp    向上轻扫
     UISwipeGestureRecognizerDirectionDown  向下轻扫
     */


    // 因为轻扫手势要求用户比较放松的扫动,因此最好不要将此手势添加到某一个视图上,会局限用户的操作
    //    添加手势
    [self.view addGestureRecognizer:swipe];
    [self.view addGestureRecognizer:swipeLeft];
}

//识别到轻扫手势后回调的方法
- (void)swipe:(UISwipeGestureRecognizer *)recognizer
{
    //当前获取中心位置
    CGPoint from = self.imageView.center;
    //目标
    CGPoint to;
    //向左轻扫
    if (recognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
        to = CGPointMake(-from.x, from.y);
    } else {//向右轻扫
        to = CGPointMake(3 * from.x, from.y);
    }
    //动画移动图片
    [UIView animateWithDuration:0.5 animations:^{
        self.imageView.center = to;
    }completion:^(BOOL finished) {
        [UIView animateWithDuration:0.5 animations:^{
            self.imageView.center = from;
        }];
    }];
}

4.3 拖动手势

- (void)addPanGesture
{
    //创建拖动手势 并添加手势的监听事件
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];

    //添加手势
    [self.imageView addGestureRecognizer:pan];
}
//识别到拖动手势后回调的方法
- (void)pan:(UIPanGestureRecognizer *)recognizer
{
    //获取手指按在图片上的位置  以图片左上角为原点
    CGPoint translation = [recognizer translationInView:self.imageView];
    //    移动图片
    recognizer.view.transform = CGAffineTransformTranslate(recognizer.view.transform, translation.x, translation.y);
    //给平移复位 因为他是在原有基础上当前递增平移 如果不复位  或清空他会越变越大
    [recognizer setTranslation:CGPointZero inView:self.imageView];
}

4.4 捏合手势

- (void)addPinchGesture
{
    //创建缩放(捏合)手势 并添加手势的监听事件
    UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
    //设置控制器为缩放手势的代理  可以实现同时识别两个手势
    pinch.delegate = self;
    [self.imageView addGestureRecognizer:pinch];
}
//识别到 缩放(捏合)手势后回调的方法
- (void)pinch:(UIPinchGestureRecognizer *)recognizer
{
    //绽放图片
    recognizer.view.transform = CGAffineTransformScale(recognizer.view.transform, recognizer.scale, recognizer.scale);

    //将缩放比例复位
    recognizer.scale = 1.0;
}

4.5 旋转手势

- (void)addRotateGesture
{
    //创建缩放 旋转并添加手势的监听事件
    UIRotationGestureRecognizer *rotate = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(rotate:)];
    //设置控制器为缩放手势的代理  可以实现同时识别两个手势
    rotate.delegate = self;
    //    添加手势
    [self.imageView addGestureRecognizer:rotate];
}
//识别到旋转手势后的回调方法
- (void)rotate:(UIRotationGestureRecognizer *)recognizer
{
    //图片旋转
    recognizer.view.transform = CGAffineTransformRotate(recognizer.view.transform, recognizer.rotation);

    //将手势识别的旋转角度复位
    recognizer.rotation = 0.0;  //非常重要  角度也会叠加
}

4.6 单击手势

- (void)addTapGesture
{
    //创建缩放点按(单击,点击)并添加手势的监听事件
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];
    //    添加手势
    [self.imageView addGestureRecognizer:tapGesture];

}
//识别到手势后的回调方法
- (void)tap
{
    NSLog(@"点我了");
}

4.7 手势的总结

  • 一定记住设置完transform之后,需要将对应的形变参数复位
  • 手势识别,是单独添加到某一个视图上的
  • 如果要同时支持多个手势识别,需要设置手势识别的代理

是否支持多手势触摸的代理方法

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

谢谢您看完这么长一篇文章,辛苦啦~!点个赞吧。哈哈👌