[New learn] 手势

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[New learn] 手势相关的知识,希望对你有一定的参考价值。

1.简介

  我们经常会在设备上查看图片等, 也会经常将图片通过手指的捏合打开来缩小和方法图片。这就是ios中的手势功能在起作用。

那么手势好像也是一种touch事件,那和UIResponder中定义的touch事件有何关联呢?是不是同一个东西呢?这篇文章将会做解答。

 

2.手势与内置手势

技术分享

在系统中,手势的基类为UIGestureRecognizer,而这个类继承自NSObject,内置手势总共六个,其中UIScreenEdgePanGestureRecognizer继承自UIPanGestureRecognizer,其余都是继承自UIGestureRecognizer。他们分别是:

  1. UITapGestureRecognizer: 观察view上发生的轻敲手势。它可以处理单次或者多次的轻敲手势动作,不管是单点还是多点。轻敲手势是用户最常用的手势之一。
  2. UISwipeGestureRecognizer: 滑动手势是另外一个很重要的用户手势了,这个类就是为此而生的,当手指在屏幕上超一个方向(上下左右)滑动的时候既能被此类实例识别到。最常用的场景就是在相册应用中,我们经常通过他来切换照片。
  3. UIPanGestureRecognizer: 拖动手势,它经常被用于拖动view去不同位置。
  4. UIPinchGestureRecognizer: 当你在照片应用上浏览照片的时候你可能会经常使用两个手指进行放大和缩小,这个时候其实就是触发了捏合手势。正如你所理解的那样,捏合需要两个手指。此类的对象为我们提供方便的处理方法。使用捏合手势的例子有很多,你可以在你的应用中实现扩大和缩小照片的功能。
  5. UIRotationGestureRecognizer: 使用两个手指来对view进行旋转,比如旋转照片等。
  6. UILongPressGestureRecognizer:这个类的对象将监视发生在view上的长按手势。按下动作必须持续足够长的事件以使得此对象能够识别出,并且在持续按下的过程中手指不能滑动出离开按下点太多的距离。
  7. UIScreenEdgePanGestureRecognizer: 这个类很想滑动手势,但是他们两者的最大区别是:这个类手势只会识别从屏幕边上向内触发的滑动手势。

以上内置的手势类可以相互组合,如长按并拖动可以UILongPressGestureRecognizer+UIPanGestureRecognizer方式组合。

 

3.手势识别机制

测试代码:https://github.com/xufeng79x/GestureEventDelivery

在手势子类介绍中我们大致可以得到两个信息:

1.手势只作用在view上

2.手势与UIResponder之间没有关系。两者是相互独立的,手势并不是UIResponder的touch***处理方式的另外一种处理方式,应为手势类并没有集成UIResponder基类。

那么手势无论如何也是要手指摸才能相应的,并且是作用在一个UIView上的,而UIView又集成自UIResponder,那么当一个触摸事件传过来的时候手势和UIResponder的touch***处理方法之间是如何分辨到底有谁来处理的呢?

技术分享

 

一般的当手势依附于某个UIView对象上的时候将会拦截本应该有此视图对象自行处理的触摸事件。可以将手势认为是一个UIView上的触摸事件的拦截器,由其先辨识到底此触摸是否符合当前view所持有的手势的特征,如果符合则讲出黎手势action,并不会讲此触摸事件继续往下传递。如果不符合则将继续交有该view的touch事件进行处理。

需要注意的是

1.【已证明,观点正确】触摸事件的被拦截是发生在手势已经成功被识别出后发生的,手势被识别出之前,view的touchBegan方法还是会接受到事件的。

2.【已证明,观点错误】从图中可以看出touch事件的寻主是找到view层级的最上层然后让这个最上层的view去相应事件,这个过程称之为hit-testing,初始处理事件的view为hit-test view。那么对于手势来讲

他将从UIApplication代理开始就不断的去拦截touch事件,换句话我们可以得出结论,父view通过手势被拦截的事件将不会在往子view中传。

 

证明一:1.触摸事件的被拦截是发生在手势已经成功被识别出后发生的,手势被识别出之前,view的touchBegan方法还是会接受到事件的。

为此我们新建一个工程加以测试:

a.新建工程,view如下图进行组合:

技术分享

b.新建view子类,覆盖所有tuch方法,并将此view类作用用于storyborad生成的view2和view3上。

#import "TestView.h"

@implementation TestView

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"in touchBegan of %@", self.name);
}

-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"in touchesMoved of %@", self.name);
}

-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"in touchesEnded of %@", self.name);
}

-(void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"in touchesCancelled of %@", self.name);
}

@end

 

 

c.在view3上增加双击手势,既增加轻敲手势设置的条件为2次,当辨识出手势后将调用action doubleTapHandleForView3去打印信息

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view2.name = @"view2";
    self.view3.name = @"view3";
    
    // view3为view层级的最顶层,在此view上增加一个双击手势
    // 1.创建双击手势
    UITapGestureRecognizer *doubleTapRecongnizer = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(doubleTapHandleForView3:)];
    
    // 2.为这个tap手势设置条件,既连续轻敲两下会被识别到
    doubleTapRecongnizer.numberOfTapsRequired = 2;
    
    // 3.在指定view上加载此手势
    [self.view3 addGestureRecognizer:doubleTapRecongnizer];
    
}

// 当双击手势被识别后将处罚此方法
-(void)doubleTapHandleForView3:(UIGestureRecognizer *)gr
{
    NSLog(@"I am in doubleTapHandleForView3");
}

 

 

d.运行程序,款速点击view3两次后结果控制台输出结果如下:

2016-02-14 14:10:55.663 GestureEventIntercept[1335:58371] in touchBegan of view3
2016-02-14 14:10:55.961 GestureEventIntercept[1335:58371] I am in doubleTapHandleForView3
2016-02-14 14:10:55.961 GestureEventIntercept[1335:58371] in touchesCancelled of view3

 

 

分析:可以看到touchBegan依然会被调用,原因为当我们首次点击view3的时候并不会认为是双击的轻敲手势,于是触摸事件依然会有touch**处理,当用户点击第二次的时候依附于view3的轻敲手势就被识别出用户行为,辨识出这是一种设定的手势,于是拦截并处理触摸事件。当手势执行完毕后view3继续能够接受和执行touch**方法。

如何避免:我们证明了此观点,那么我们如何没避免这种情况呢?这种情况一般会出现手势被识别前,有时候也会带来一些体验上的问题。一般地可以按照如下方法:

    // 使得touchBegan方法延迟执行
    doubleTapRecongnizer.delaysTouchesBegan = YES;

 通过设定延迟此属性值让window延迟转发触摸事件给view,当在延迟期间手势识别到后,就不再向view转发触摸事件了。

运行双击view3后的结果:

2016-02-14 14:20:15.665 GestureEventIntercept[1371:61183] I am in doubleTapHandleForView3

 

总结:所以我们证明了这个观点,当手势识别是有一个事件过程和条件满足过程的,在未被识别之前还是会想view转发触摸事件,当被识别后将会贪婪的拦截所有触摸事件。

 

证明二:【将被证实为假】2.从图中可以看出touch事件的寻主是找到view层级的最上层然后让这个最上层的view去相应事件,这个过程称之为hit-testing,初始处理事件的view为hit-test view。那么对于手势来讲他将从UIApplication代理开始就不断的去拦截touch事件,换句话我们可以得出结论,父view通过手势被拦截的事件将不会在往子view中传。

 

a.hiting-view过程是会将触摸事件由application单例到window再到controller、父view一直传递到层级最上层的子View然后去处理的,而手势则会在传递过程中就拦截掉触摸事件,吗??!!

 

b.在上述例子中view2是view3的superview,我们在view2上也和view3一样增加同样的双击手势:

    // 3.在指定view上加载此手势
    [self.view3 addGestureRecognizer:doubleTapRecongnizer];
    
    
    // view2为view3父view,我们为view2增加同view3一样的双击手势
    // 1.创建双击手势
    UITapGestureRecognizer *doubleTapRecongnizer1 = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(doubleTapHandleForView2:)];
    
    // 2.为这个tap手势设置条件,既连续轻敲两下会被识别到
    doubleTapRecongnizer1.numberOfTapsRequired = 2;
    
    // 使得touchBegan方法延迟执行
    doubleTapRecongnizer1.delaysTouchesBegan = YES;
    
    // 3.在指定view上加载此手势
    [self.view2 addGestureRecognizer:doubleTapRecongnizer1];
    
}

 

c.增加view2的双击手势处理action:

// 当双击手势被识别后将处罚此方法
-(void)doubleTapHandleForView2:(UIGestureRecognizer *)gr
{
    NSLog(@"I am in doubleTapHandleForView2");
}

 

 

 

d.运行程序,双击view3,预想应该输出“I am in doubleTapHandleForView2”,但是实际上:

2016-02-14 15:43:12.551 GestureEventIntercept[1677:85051] I am in doubleTapHandleForView3

 

 

总结1:所以我们得出原有观点错误,手势都会首先在hit-view上响应,如同触摸事件一样。

 

E.那么如果将view3的手势去掉,在运行应用会打印view3的touch**信息吗?我们来测试一下:

注释掉view3的手势:

    // 使得touchBegan方法延迟执行
    doubleTapRecongnizer.delaysTouchesBegan = YES;
    
    // 3.在指定view上加载此手势
    //[self.view3 addGestureRecognizer:doubleTapRecongnizer];
    
    
    // view2为view3父view,我们为view2增加同view3一样的双击手势
    // 1.创建双击手势
    UITapGestureRecognizer *doubleTapRecongnizer1 = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(doubleTapHandleForView2:)];

 

 

F.运行程序,双击view3,预想应该输出view3的touch**内的日志才对,但是实际上输出了父view的手势信息:

2016-02-14 15:50:12.678 GestureEventIntercept[1709:88624] I am in doubleTapHandleForView2

 

 

总结2:当当前view无手势,或者手势不识别的时候,将会向上去寻找父view的手势内容,知道便利完所有父view后都没有响应手势响应,则将执行当前view的touch**方法。

 

4.手势组合

代码:https://github.com/xufeng79x/GestureCombination

手势作用于UIView上,一个UIView对象可以加载多个手势,最为经典的莫过于长按后拖动操作。

 a.首先创建工程GestureCombination,他被设计为有一个黑色的view,当长按于这个view的时候变为绿色,当长按并拖动的时候view会随之移动,当放开手指的时候view再次变为默认黑色。

 b.为了达到上述目的,则需要两个手势:长按手势和拖动手势

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
    
    //  长按是需要一个过程的,一般默认按下0.5秒后才能被识别出,为了避免在识别出之前做touchBegan事件,需要设定延迟转发
    [self.longPressGesture setDelaysTouchesBegan:YES];
    
    [self.testView addGestureRecognizer:self.longPressGesture];
    
    self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panView:)];
    
    [self.testView addGestureRecognizer:self.panGesture];
    
    
}

//拖动事件处理
-(void) panView:(UIGestureRecognizer *)gr
{
    NSLog(@"asfsaf");
}

// 长按事件处理
-(void) longPress:(UIGestureRecognizer *)gr
{
    // 当长按被识别到的时候,将view颜色设置为绿色
    if (gr.state == UIGestureRecognizerStateBegan) {
        self.testView.backgroundColor = [UIColor greenColor];
    }
    // 当长结束或者其他状态的时候设置为原来的黑色
    else if (gr.state == UIGestureRecognizerStateChanged){
        self.testView.backgroundColor = [UIColor greenColor];
    }
    else{
        self.testView.backgroundColor = [UIColor blackColor];
    }
}

 

我们按此完成代码后,发现当长按发生,并且view颜色也能够变成绿色,当长按并拖动的时候发现panView方法未被调用(日志为被打印出),换句话说拖动手势没有被识别,更为神奇的是touch**方法也没有被调用,这说明我们的触摸事件已经完全被长按手势给“贪婪的”拦截了。

 

c.在多手势组合中,当摸个手是被识别出后,他将拦截所有触摸事件,导致其他手势无法得到触摸事件而无法被识别。在本例中,长按后即使拖动,应为长按手势已经拦截了触摸事件,所以拖动就无法起效了。如何解决这个问题呢?

 

d.使用UIGestureRecognizerDelegate代理,将此代理赋值于拖动手势即可,同样我们无法获知源码,按照推断可以得出,其实拖动手势也已经在当前触摸事件中辨识出了属于自己的行为,但是多手势中默认禁止了这种行为,当我们覆盖如下方法,此方法相当于拖动手势识别出行为后询问【实现】我是否能够去做出处理。当次方法回答YES的时候,拖动手势就屁颠屁颠的干自己的活了。

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer

 

 在我的代码中具体代码为是:

@interface ViewController () <UIGestureRecognizerDelegate>
。。。。。。

self.panGesture.delegate = self;

。。。。。。

// 覆盖代理方法,当某个手势也辨识出当前行为时候,会调用此方法来申请是否能够参与活动
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (gestureRecognizer == self.panGesture && otherGestureRecognizer == self.longPressGesture){
        return YES;
    }
    
    return NO;
}

 

此时我们在运行程序,长按view后,view将变为绿色,此时鼠标拖动,在panView:方法中日志将被打印,哈哈,这样我们就可以将日志打印代码替换掉换成自己的逻辑代码了。

e.实现拖动逻辑

//拖动事件处理
-(void) panView:(UIPanGestureRecognizer *)gr
{
    // 在移动的时候触发处理
    if (gr.state == UIGestureRecognizerStateChanged) {
        // 当前手势移动到哪里了
        CGPoint translation = [gr translationInView:self.testView];
        CGRect frame = self.testView.frame;
        frame.origin.x += translation.x;
        frame.origin.y += translation.y;
        
        self.testView.frame = frame;
        
        // 增加此行代码后,每一次都会传来增量的移动坐标数据。
        [gr setTranslation:CGPointZero inView:self.testView];
    }
}

 

此时我们在测试一下我们的应用:

技术分享

 

f.我以为我可以屁颠屁颠的去发布博客了,但是我错了,我遇到一个麻烦, 细心的读者可能会发现,即使没有长按让view变色,上手就拖动view也能够移动view,但是我们的设计是说需要在长按后才能拖动不是吗?所以我增加了一个长按标记位,然后在长按发生过程中设置为YES,其他状态设置为NO

// 手势限制
@property (nonatomic,assign) BOOL panEnable;

。。。。。

// 长按事件处理
-(void) longPress:(UILongPressGestureRecognizer *)gr
{
    // 当长按被识别到的时候,将view颜色设置为绿色
    if (gr.state == UIGestureRecognizerStateBegan) {
        self.testView.backgroundColor = [UIColor greenColor];
        self.panEnable = YES;
    }
    // 当长按并移动手指的时候保持颜色不变
    else if (gr.state == UIGestureRecognizerStateChanged){
        self.testView.backgroundColor = [UIColor greenColor];
        self.panEnable = YES;
    }
    // 当长结束或者其他状态的时候设置为原来的黑色
    else{
        self.testView.backgroundColor = [UIColor blackColor];
        self.panEnable = NO;
    }
}
。。。。

 

然后想当然的在拖动手势的代理方法中作出判断,如果此时标记位为YES则可以返回YES,具体的逻辑是这样的:

// 覆盖代理方法,当某个手势也辨识出当前行为时候,会调用此方法来申请是否能够参与活动
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (gestureRecognizer == self.panGesture &&  self.panEnable){
        return YES;
    }
    
    return NO;
}

 

但是测试的结果出乎我的意料,不进行长按而直接能够拖动的问题依然存在。上述方法中,即使我没有逻辑,直接返货NO,问题也依然存在

或者可以说是“因祸得福”我得出了这个结论:

 

在多手势情况下,运用手势代理来进行多手势并行是需要条件的,既当前手势如果是第一手势(既直接第一个被辨识出的手势)的话,代理内的方法逻辑是不起效的。

 

所以此种情况下我使用了第二种方法来处理,既在拖动处理方法中做判断:

//拖动事件处理
-(void) panView:(UIPanGestureRecognizer *)gr
{
    if (!self.panEnable) {
        return;
    }

 

 

5.手势冲突

手势冲突在多手势场景下也进场发生,如:

单机和双击轻敲组合

Swipe和Pan的组合等

解决这种场景我们可以使用手势
- requireGestureRecognizerToFail

方法来将几种手势穿起来,如 

[轻敲一次手势 requireGestureRecognizerToFail: 轻敲两次手势]

 这样就可以解决系统将轻敲两次手势辨识为两次轻敲一次手势了,系统运行的时候会优先去考两次轻敲两次是否成功,如果成功则不会再去辨识轻敲一次手势了,其他组合机制也是一样。 

(我在想一个问题,为什么苹果不出一个叫做requireGestureRecognizerToOk)的方法,这样我长按拖动的问题不就解决了!!!!)

6.自定义手势

暂略。

 

以上是关于[New learn] 手势的主要内容,如果未能解决你的问题,请参考以下文章

[New Learn]被嫌弃的app的一生

[New learn] NSOperation基本使用

[New learn]AutoLayout调查

[New learn] 网络基础-网络操作

[New learn]AutoLayout调查基于code

Learn a new language