iOS-Events[1]-Gesture Recognizers

本文对Gesture Recognizers的机制进行讲解。

iOS系统的Events类型分为以下几种:Multitouch events, Accelerometer events, Remote control events。

Events

Gesture Recognizers

UIKit提供了Gesture Recognizers,可以直接对Events进行处理。Gesture Recognizers把底层的事件处理封装成更上层的Actions 对象以供开发者调用。Gesture Recognizers判断触碰是否是一个特定的手势,例如滑动(swipe),缩放(pinch)和旋转(rotation),如果是,则发送一个Action消息给对应的监听对象,一般是ViewController。

Gesture Recognizers

分类

Gesture UIKit class
Tapping (any number of taps) UITapGestureRecognizer
Pinching in and out (for zooming a view) UIPinchGestureRecognizer
Panning or dragging UIPanGestureRecognizer
Swiping (in any direction) UISwipeGestureRecognizer
Rotating (fingers moving in opposite directions) UIRotationGestureRecognizer
Long press (also known as “touch and hold”) UILongPressGestureRecognizer

手势分为单次(Discrete)和连续(Continuous)的两大类。对于单次的,只会发送一次Action,而对于连续的,会多次发送。

Discrete and Continuous Gestures

状态机

所有手势起始状态都为Possible。对于Discrete的手势,则可能切换到FailedRecognized两个状态。对于Continuous的手势,则可能切换到FailedRecognizedCanceled三个状态。

State Machines

多手势

手势添加

多手势处理,可以通过以下方法:

1
2
addGestureRecognizer:
removeGestureRecognizer:

手势禁止

如果想要禁止一个手势识别,可以通过以下回调:

1
2
gestureRecognizer:shouldReceiveTouch:
gestureRecognizerShouldBegin:

两者的区别是,gestureRecognizer:shouldReceiveTouch:是在触碰开始时触发,因此不会改变手势的状态,而gestureRecognizerShouldBegin:是在手势即将从Possible变成其他状态时触发。因此,如果一开始就能判断要禁止手势,使用gestureRecognizer:shouldReceiveTouch:,如果需要延后判断,则使用gestureRecognizerShouldBegin:

手势冲突

对于手势可能产生冲突的问题,可以用以下回调:

1
2
gestureRecognizer:shouldRequireFailureOfGestureRecognizer:
gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:

例如,Pan和Swipe都是滑动,如果想要优先识别Swipe,则可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface ViewController ()<UIGestureRecognizerDelegate>
@end

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];

UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan)];
panGR.delegate = self;
[self.view addGestureRecognizer:panGR];

UISwipeGestureRecognizer *swipeGR = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe)];
swipeGR.delegate = self;
[self.view addGestureRecognizer:swipeGR];
}

- (void)pan
{
NSLog(@"pan...");
}

- (void)swipe
{
NSLog(@"swipe...");
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UISwipeGestureRecognizer class]]){
return YES;
}
return NO;
}

手势并发

对于两个可以同时进行的手势,例如Pin和Rotate,可以使用以下回调:

1
gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:

手势排斥

对于两个单向关系的手势,例如Rotate手势可能排斥Pin,可以使用以下回调:

1
[rotationGestureRecognizer canPreventGestureRecognizer:pinchGestureRecognizer];

控件手势

对于iOS系统的内置控件,如果手势与控件的事件发生冲突,则控件的事件会被优先触发:

  • Tap与UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl;
  • Swipe与UISlider;
  • Pan与UISwitch。

如果希望手势优先被触发,则可以重写内置控件。

UITouch & UIEvent

在iOS系统上,一个UITouch代表着单根手指在屏幕上的按压或者移动。一个手势包含一个或者多个UITouch对象,例如,Pin手势包含两个。

一个UIEvent包含在一次多触摸序列中的所有的UITouch。一次多触摸序列是指当有一个手指触碰屏幕到最后一根手指离开屏幕。当手指移动时,iOS会发送UITouch到UIEvent,并不断更新UITouch的属性,例如状态、位置和时间戳等。

Touch Events

在一次多触摸序列中,App会调用以下回调:

1
2
3
4
touchesBegan:withEvent:一根或多根手指触碰屏幕;
touchesMoved:withEvent:手指移动;
touchesEnded:withEvent:一根或多根手指离开屏幕;
touchesCancelled:withEvent:被系统事件取消,例如来电等;

注意,手势的状态并不依赖于UITouch的状态,两者有各自的判断标准

自定义手势

自定义手势,首先,新建一个UIGestureRecognizer的子类,然后根据UITouch事件来判断是否是该手势,另外,还需要实现reset方法来进行手势能复原到原始状态。

下面是一个打钩手势的识别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#import <UIKit/UIGestureRecognizerSubclass.h>

// Implemented in your custom subclass
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
if ([touches count] != 1) {
self.state = UIGestureRecognizerStateFailed;
return;
}
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesMoved:touches withEvent:event];
if (self.state == UIGestureRecognizerStateFailed) return;
CGPoint nowPoint = [touches.anyObject locationInView:self.view];
CGPoint prevPoint = [touches.anyObject previousLocationInView:self.view];

// strokeUp is a property
if (!self.strokeUp) {
// On downstroke, both x and y increase in positive direction
if (nowPoint.x >= prevPoint.x && nowPoint.y >= prevPoint.y) {
self.midPoint = nowPoint;
// Upstroke has increasing x value but decreasing y value
} else if (nowPoint.x >= prevPoint.x && nowPoint.y <= prevPoint.y) {
self.strokeUp = YES;
} else {
self.state = UIGestureRecognizerStateFailed;
}
}
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
if ((self.state == UIGestureRecognizerStatePossible) && self.strokeUp) {
self.state = UIGestureRecognizerStateRecognized;
}
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
self.midPoint = CGPointZero;
self.strokeUp = NO;
self.state = UIGestureRecognizerStateFailed;
}

- (void)reset {
[super reset];
self.midPoint = CGPointZero;
self.strokeUp = NO;
}

Touches分发机制

iOS系统上,Touches的分发机制如下:

1
UIApplication->UIWindow->Gesture Recognizer->View

Touches-Path

注意,Gesture RecognizerUITouch回调有更高的优先级,如果,Gesture Recognizer识别成功了,UITouch回调不会进入Ended状态,而会进入Cancelled。如果识别失败了,则Gesture Recognizer会进入Failed状态,而UITouch回调进入Ended状态。

例如,一个Tap手势和UITouch的事件顺序如下:

Touches

这个事件顺序是通过两个属性控制的,分别是:

  • delaysTouchesBegan:默认是NO,所以,Gesture Recognizer和View的Touch回调会同时进入Began状态,如果希望View的Touch回调晚点进入Began状态,则可以修改为YES,例如,Tap手势和UITouch;
  • delaysTouchesEnded:默认是YES,所以,View的Touch回调会延迟进入Ended状态,如果Gesture Recognizer识别成功了,则直接进入Cancelled状态。在有需要时,也可以修改为NO。

如果Gesture Recognizer判断识别失败了,则可以调用以下方法,让View的Touch回调立刻被调用:

1
ignoreTouch:forEvent: