iOS的触摸事件

本文最后更新于:2 年前

iOS有三种事件类型:

  • Touch Events(触摸事件)
  • Motion Events(传感器运动事件,比如重力感应和摇一摇等)
  • Remote Events(远程事件,比如用耳机上得按键来控制手机)

我主要谈谈触摸事件,触摸事件的整个过程可以分为传递和响应2个阶段:

  • 传递:是当我们触摸屏幕时,为我们找出最适合的view
  • 响应:当我们找出最适合的view后,但未必此view可以响应触摸事件,所以需要继续找出能响应此事件的view

一、传递过程

基本上我们所能看到的所有图形界面都是继承自UIResponder的,UIResponder管理用户的操作事件的分发。

我们每个应用的视图结构中,一个视图可以有多个子视图,一个子视图同一时刻只有一个父视图,这叫N叉树,而每一个继承UIResponder的对象都可以在这个N叉树中扮演一个节点,当某个节点成为最高响应者的时候,从这个节点开始往其父节点开始追朔出一条链,那么对于这一个节点来讲,这一条链就是当前的响应者链。响应者链将系统捕获到的UIEventUITouch从叶节点开始层层向上传递,期间可以选择停止传递,也可以选择继续。

例如:

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
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

AButton *button = [[AButton alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
button.backgroundColor = [UIColor lightGrayColor];
[button addTarget:self action:@selector(clickAction:) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:button];
}

- (void)clickAction:(AButton *)sender {
NSLog(@"%@",sender.nextResponder);
NSLog(@"%@",sender.nextResponder.nextResponder);
NSLog(@"%@",sender.nextResponder.nextResponder.nextResponder);
NSLog(@"%@",sender.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"%@",sender.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
NSLog(@"%@",sender.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder);
}

2019-01-25 17:24:54.473862+0800 touch1[16677:338125] <UIView: 0x7fc54ee180e0; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x60c000032f80>>
2019-01-25 17:24:54.474111+0800 touch1[16677:338125] <ViewController: 0x7fc551803b90>
2019-01-25 17:24:54.474441+0800 touch1[16677:338125] <UIWindow: 0x7fc5518045d0; frame = (0 0; 375 812); gestureRecognizers = <NSArray: 0x608000442550>; layer = <UIWindowLayer: 0x60800002cf60>>
2019-01-25 17:24:54.474593+0800 touch1[16677:338125] <UIApplication: 0x7fc551900000>
2019-01-25 17:24:54.474722+0800 touch1[16677:338125] <AppDelegate: 0x60000002ec60>
2019-01-25 17:24:54.474835+0800 touch1[16677:338125] (null)

通过层层打印nextResponder去寻找响应链,我们可以找到一条响应链,如图

实际上我们要把这棵树写完整,应该还要算上UIButtonUILabelUIImageView,因为他们也是UIReponder的子类,这里就先不考虑了。

二、Hit-Testing View

有了事件响应链,接下来的就是寻找响应事件的具体响应者,我们称之为:Hit-Testing View,寻找最终相应事件的过程我们称着为Hit-Test,系统有一个层层调用的方法,如下

1
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

通过这个方法,系统会顺着响应链层层调用,直到响应链中某一个节点的方法返回了nil,那么证明传递过程中断,即找到了响应事件的view层了,该方法内部的逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}

例如:可以通过重写hitTest:withEvent:的方法阻止响应链中最顶层的view作为响应者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

AButton *aButton = [[AButton alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
aButton.backgroundColor = [UIColor redColor];
[aButton addTarget:self action:@selector(clickABtn:) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:aButton];

BButton *bButton = [[BButton alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
bButton.backgroundColor = [UIColor greenColor];
[bButton addTarget:self action:@selector(clickBBtn:) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:bButton];
}

- (void)clickABtn:(id)sender {
NSLog(@"AButton 点击");
}

- (void)clickBBtn:(id)sender {
NSLog(@"BButton 点击");
}

综上,每当手指接触屏幕,UIApplication接收到手指的事件之后,就会去调用UIWindowhitTest:withEvent:方法,看看当前点击的点是不是在window内,如果是则继续依次调用subViewhitTest:withEvent:方法,直到响应链中断的前一个view或者调用到了最顶层的view,此时这个viewview上面依附的手势,都会和一个UITouch的对象关联起来,这个UITouch会作为事件传递的参数之一,我们可以看到UITouch头文件里面有一个viewgestureRecognizers的属性,就是hitTest view和它的手势。

三、Hit-Test的应用

  1. 重写hitTest:withEvent:方法阻断事件传递,实现指定效果

  2. view和子view响应同一事件

重写子viewhitTest:withEvent:方法,触发时同时调用super hitTest:withEvent:方法即可

四、其他

1、UITouch

  • UITouch对象记录 触摸的位置、时间、阶段。
  • 一根手指对应一个UITouch对象。
  • 手指移动时,系统会更新同一个UITouch对象。
  • 手指离开屏幕时,UITouch对象被销毁。

2、UIEvent

UIEvent是事件对象,每产生一个事件,就会产生一个UIEvent对象。记录事件产生的时刻和类型

1
2
3
4
5
6
7
8
9
10
// Generally, all responders which do custom touch handling should override all four of these methods.(通常,所有的触摸操作都应该包含这四种方法。)
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each(您的响应者将收到touchesEnded:withEvent:或者touchesCancelled:withEvent:)
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to(你必须处理已取消的触摸以确保程序中的正确行为)
// do so is very likely to lead to incorrect behavior or crashes.(这样做很可能导致错误的行为或崩溃。)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
  • 以上4个方法中,都有UITouchUIEvent两个类型的参数,一次完整的触摸过程中,4个触摸方法都是同一个UIEvent参数
  • 如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象,如果这两根手指一前一后分开触摸,那么分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象,所以根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸

五、UIGestureRecognizer抽象类

UIGestureRecognizer将一些和手势操作相关的方法抽象了出来,但它本身并不实现什么手势,因此,在开发中,我们一般不会直接使用UIGestureRecognizer的对象,而是通过其子类进行实例化

常用手势

  • UITapGestureRecognizer(点击)
  • UILongPressGestureRecognizer(长按)
  • UISwipeGestureRecognizer(轻扫)
  • UIRotationGestureRecognizer(旋转)
  • UIPinchGestureRecognizer(缩放)
  • UIPanGestureRecognizer(拖拽)

1、初始化

这些手势在初始化时都采用父类的方法进行初始化

1
2
3
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action NS_DESIGNATED_INITIALIZER;
- (void)addTarget:(id)target action:(SEL)action;
- (void)removeTarget:(nullable id)target action:(nullable SEL)action;

其中重复调用addTarget:action:方法会同时触发各自的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.

UITapGestureRecognizer *tapGR = [[UITapGestureRecognizer alloc] init];
[tapGR addTarget:self action:@selector(clickAction1:)];
[tapGR addTarget:self action:@selector(clickAction2:)];
[self.view addGestureRecognizer:tapGR];
}

- (void)clickAction1:(id)sender {
NSLog(@"clickAction1");
}

- (void)clickAction2:(id)sender {
NSLog(@"clickAction2");
}

2019-01-28 11:24:37.196729+0800 touch5[10439:342134] clickAction1
2019-01-28 11:24:37.196920+0800 touch5[10439:342134] clickAction2

2、属性

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
//手势的状态
@property(nonatomic,readonly) UIGestureRecognizerState state;
//手势代理
@property(nullable,nonatomic,weak) id <UIGestureRecognizerDelegate> delegate;
//手势是否有效 默认YES
@property(nonatomic, getter=isEnabled) BOOL enabled;
//获取手势所在的view
@property(nullable, nonatomic,readonly) UIView *view;
//取消view上面的touch事件响应 default YES **下面会详解该属性** 如果识别到了手势,系统将会发送touchesCancelled:withEvent:消息,终止触摸事件的传递。也就是说默认当识别到手势时,touch事件传递的方法将被终止,如果设置为NO,touch事件传递的方法仍然会被执行。
@property(nonatomic) BOOL cancelsTouchesInView;
//延迟touch事件开始 default NO **下面会详解该属性**用于控制事件的开始响应的时机,"是否延迟响应触摸事件"。设置为NO,不会延迟响应触摸事件,如果我们设置为YES,在手势没有被识别失败前,都不会给事件传递链发送消息。
@property(nonatomic) BOOL delaysTouchesBegan;
//延迟touch事件结束 default YES **下面会详解该属性** 用于控制事件结束响应的时机,"是否延迟结束触摸事件",设置为NO,则会立马调用touchEnd:withEvent这个方法(如果需要调用的话)。设置为YES,会等待一个很短的时间,如果没有接收到新的手势识别任务,才会发送touchesEnded消息到事件传递链。
@property(nonatomic) BOOL delaysTouchesEnded;
//允许touch的类型数组,**下面会详解该属性**
@property(nonatomic, copy) NSArray<NSNumber *> *allowedTouchTypes
//允许按压press的类型数组
@property(nonatomic, copy) NSArray<NSNumber *> *allowedPressTypes
//是否只允许一种touchType 类型,**下面会详解该属性**
@property (nonatomic) BOOL requiresExclusiveTouchType
//手势依赖(手势互斥)方法,**下面会详解该方法**
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;
//获取在传入view的点击位置的信息方法
- (CGPoint)locationInView:(nullable UIView*)view;
//获取触摸点数
@property(nonatomic, readonly) NSUInteger numberOfTouches;
//(touchIndex 是第几个触摸点)用来获取多触摸点在view上位置信息的方法
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(nullable UIView*)view;
// 给手势加一个名字,以方便调式(iOS11 or later可以用)
@property (nullable, nonatomic, copy) NSString *name API_AVAILABLE(ios(11.0)

3、UIGestureRecognizerDelegate代理方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 当手势识别器尝试转换出UIGestureRecognizerStatePossible时调用。 返回NO会导致它转换为UIGestureRecognizerStateFailed
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

// 当识别其中一个gestureRecognizer或otherGestureRecognizer时,会被另一个人阻止
// 返回YES以允许两者同时识别。 默认实现返回NO(默认情况下,不能同时识别两个手势)
//
// 注意:保证返回YES可以同时识别。 返回NO不能保证阻止同时识别,因为另一个手势的代表可能返回YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

// 每次尝试识别时调用一次,因此可以懒惰地确定故障要求,并且可以跨视图层次结构在识别器之间建立故障要求
// 返回YES以在gestureRecognizer和otherGestureRecognizer之间设置动态失败要求
//
// 注意:保证返回YES以设置故障要求。 返回NO并不保证不存在失败要求,因为其他手势的对应委托或子类方法可能返回YES
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

// 在touchesBegan之前调用:withEvent:在手势识别器上调用以获得新的触摸。 返回NO以防止手势识别器看到此触摸
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

// 在pressBegan:withEvent:之前调用,在新的印刷机的手势识别器上调用。 返回NO以防止手势识别器看到此按下
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;