UI 界面iOS开发技术分享Touch事件

深入理解touch事件和手势的关系

2017-03-24  本文已影响2370人  喵子G

1,手势和touch事件的先后关系

测试视图结构

为控制器、LightGrayView、RedView、GreenView、YellowView添加手势监听事件:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"rootView touchBegan");
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"rootView view touchCancelled");
    [super touchesCancelled:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"rootView view touchMoved");
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"rootView view touchEnded");
    [super touchesEnded:touches withEvent:event];
}

在控制器中为LightGrayView添加一个手势:

    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(jkr_tapGestureAction:)];
    [self.LightGrayView addGestureRecognizer:tapGesture];
…
- (void)jkr_tapGestureAction:(UITapGestureRecognizer *)tapGusture {
    NSLog(@"%@", tapGusture);
}

点击RedView,输出如下:

yellow view is inside: 0
yellow view hit: (null)
lightGray view is inside: 1
green view is inside: 0
green view hit: (null)
red view is inside: 1
red view hit: <RedView: 0x7fb33a509680; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x600000033420>>
lightGray view hit: <RedView: 0x7fb33a509680; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x600000033420>>
red view touchBegan
lightGray view touchBegan
rootView touchBegan
Tap action
red view touchCancelled
lightGray view touchCancelled
rootView view touchCancelled

这里可看到,手势方法在所有响应视图的touchBegan方法执行后调用,并且手势方法执行后,所有响应视图的touch事件全部取消。
为了更详细的观察手势和touch事件的关系,现在自定义个一个获取它的tap事件的手势:

#import "JKRTapGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>

@implementation JKRTapGestureRecognizer

- (instancetype)initWithTarget:(id)target action:(SEL)action
{
    self = [super initWithTarget:target action:action];
    self.delegate = self;
    return self;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    NSLog(@"RecognizerShouldBegin");
    return YES;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"tapgesture touchBegan");
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"tapgesture touchCancelled");
    [super touchesCancelled:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"tapgesture touchMoved");
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"tapgesture touchEnded");
    [super touchesEnded:touches withEvent:event];
}

@end

修改LightGrayView添加手势的类型为自定义手势,点击RedView查看输出:

yellow view is inside: 0
yellow view hit: (null)
lightGray view is inside: 1
green view is inside: 0
green view hit: (null)
red view is inside: 1
red view hit: <RedView: 0x7f9ce840aab0; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x60800002d480>>
2017-03-21 16:01:33.589 JKRUIResponderDemo[35855:3086959] lightGray view hit: <RedView: 0x7f9ce840aab0; frame = (0 0; 196 100); autoresize = RM+BM; layer = <CALayer: 0x60800002d480>>
tapgesture touchBegan
red view touchBegan
lightGray view touchBegan
rootView touchBegan
tapgesture touchEnded
RecognizerShouldBegin
Tap action
red view touchCancelled
lightGray view touchCancelled
rootView view touchCancelled

通过输出可以发现,手势的touch事件是优先于视图的touch事件触发,并且tap手势是在tap手势的touchEnded方法之后才触发手势的识别和响应方法的处理,在手势处理后,视图的touch事件就会取消。

结论:默认情况下,当一个touch事件发生后,如果touch事件响应者中有视图添加了手势,那么就优先处理添该视图的手势对象中的touch相关方法来处理,视图的touch事件在手势的touch事件之后处理。如果手势事件识别出来,那么手势事件之后的视图的touch事件就会取消。

2,手势和视图的关系

测试:

将LightGrayView的pointInside方法返回值设置为NO,点击RedView观察输出可以发现,当视图无法成为touch事件的响应者的时候,它的手势也是无法识别的。

结论:视图的手势的能够响应的前提是该视图能够成为touch事件的响应者。

3,delaysTouchesBegan参数的作用

该属性默认设置为NO,视图的touchesBegan方法会在手势的touchBegan事件之后执行;视图touchesMoved方法会在手势的touchesMoved方法后执行,视图的touchesEnded方法会在手势的touchesEnded方法后执行。在哪个touch方法中手势识别到并成功处理,并且cancelsTouchesInView为默认值YES,那么视图会调用touchesCancelled方法取消touch事件。
该属性如果设置为YES,那么视图的触摸事件一定是在手势的touchEnded方法之后才确认是否去执行,如果手势没有识别到,就执行视图的touch方法。如果手势识别到并成功处理并且cancelsTouchesInView属性为YES,那么视图的touch方法都不会被执行。

4,cancelsTouchesInView的作用

该属性默认设置为YES,视图的手势在识别并处理后,会取消视图的touch事件。
该属性如果设置为NO,那么视图的手势在识别并处理后,不会取消视图的touch事件,视图的touch事件继续执行。

5,手势处理应用

应用一:处理tap手势和touchesBegan之间的冲突

思考:之前看到,默认状态下,手势的touchesBegan方法虽然先于视图的touchesBegan方法执行,但是手势的识别却是在视图的touchesBegan方法之后执行的,即手势虽然touch方法的处理优先级高于视图,但是手势还是不能够拦截视图的touchesBegan方法的。

修改RedView的touchBegan方法,弹出一个弹框:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"red view touchBegan");
    [super touchesBegan:touches withEvent:event];
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"touch" message:nil delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
    [alertView show];
}

给控制器的View加一个pan手势:

    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
    [self.view addGestureRecognizer:panGesture];

点击RedView,直接就弹出了弹框,并不能处理滑动事件,并且控制器的View手势事件也不会回调了。

现在就需要让手势的识别和处理优先级高度视图的touchesBegan方法,之前我们知道,默认状态下,手势的touchesBegan方法执行后马上就回执行视图的touchesBegan方法,所以手势还没有识别就调用了RedView的touchesBegan方法弹出了弹窗。现在设置delaysTouchesBegan为YES,让视图的touchesBegan方法等待到手势事件处理完毕执行touchesEnded方法后才能调用:

panGesture.delaysTouchesBegan = YES;

设置该属性后,点击了RedView,手势识别了touchesBegan和touchesEnded方法,并没有走touchesMoved方法,也没有成功识别到手势。然后再走RedView的touchesBegan方法,执行弹窗操作。现在就实现了点击RedView后弹出弹窗,在RedView上面滑动调用滑动方法处理滑动事件。

应用二:处理pan手势和touchesMoved手势之间的冲突

移除RedView的手势和touchesBegan中的弹窗方法,在RedView中添加一个layer,通过touchesMoved方法改变layer的位置:

@interface RedView ()

@property (nonatomic, strong) CALayer *jkr_layer;
@property (nonatomic, assign) CGPoint jkr_layerCenter;

@end

@implementation RedView

- (void)drawRect:(CGRect)rect {
    [super drawRect:rect];
    NSString *index = @"4";
    [index drawInRect:rect withAttributes:@{NSForegroundColorAttributeName:[UIColor blackColor], NSFontAttributeName:[UIFont systemFontOfSize:18]}];
    self.jkr_layer.frame = CGRectMake(self.jkr_layerCenter.x - 10, self.jkr_layerCenter.y - 10, 20, 20);
    [self.layer addSublayer:self.jkr_layer];
}

- (CALayer *)jkr_layer {
    if (!_jkr_layer) {
        _jkr_layer = [CAShapeLayer new];
        _jkr_layer.frame = CGRectMake(0, 0, 20, 20);
        _jkr_layer.backgroundColor = [UIColor purpleColor].CGColor;
    }
    return _jkr_layer;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"red view is inside: %zd", isInside);
    return isInside;
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    NSLog(@"red view hit: %@", view);
    return view;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"red view touchBegan");
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"red view touchCancelled");
    [super touchesCancelled:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    UITouch *touch = touches.anyObject;
    CGPoint touchPoint = [touch locationInView:self];
    self.jkr_layerCenter = touchPoint;
    [self setNeedsDisplay];
    NSLog(@"red view touchMoved");
    [super touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"red view touchEnded");
    [super touchesEnded:touches withEvent:event];
}

@end

在RedView中滑动屏幕就可以看到紫色方块跟随手指移动。

现在给RedView添加一个pan手势:

UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
[self.RedView addGestureRecognizer:pan];

在RedView中滑动屏幕,发现滑动一个很短的距离后紫色方块不能够跟随手指移动了。
观察输出发现当pan手势开始第一次调用滑动方法的时候,视图的touch事件被取消了:

…
rootView view touchMoved
Pan action
red view touchCancelle
![Uploading pan_750631.gif . . .]d
…

这是因为手势有一个属性cancelsTouchesInView,这个属性的做用就是决定是否当手势关联的方法调用的时候,调用视图的touchesCancelled:withEvent:方法取消视图的touch事件,默认值为YES。
现在修改这个值为NO:

pan.cancelsTouchesInView = NO;

现在就可以在处理视图的滑动手势的同时处理视图的touchesMoved方法。

pan.gif

6,手势之间的共存和互斥

现在有一个需求,需要为RedView添加两个手势,分别识别单击和双击,创建两个tap手势给RedView添加上,一个做单击处理,一个做双击处理:

    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(singleAction)];
    [self.RedView addGestureRecognizer:singleTap];
    
    UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleAction)];
    doubleTap.numberOfTapsRequired = 2;
    [self.RedView addGestureRecognizer:doubleTap];
//处理方法
- (void)singleAction {
    NSLog(@"single action");
}

- (void)doubleAction {
    NSLog(@"double action");
}

要让两个手势区分开来用如下方法:

[singleTap requireGestureRecognizerToFail:doubleTap];

即让双击手势不成功识别的情况下才回调用单击手势。

获取授权

上一篇下一篇

猜你喜欢

热点阅读