3、点击手势:UITapGestureRecognizer 配置、多击与多指识别
点击手势,说白了就是 iOS 开发里最基础、最常用的手势。你想想看,用户点一下按钮、双击图片、三指同时点击……这些操作背后都是 UITapGestureRecognizer 在干活。我个人习惯把它叫做「轻点手势」,因为它确实很「轻」——配置简单,但坑也不少。
3.1 基础配置:一行代码搞定?没那么简单
先看个最基础的例子。给一个 UIView 添加单击手势:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleTap:)];
[self.view addGestureRecognizer:tap];
嗯,确实就这几行。但我在项目中遇到过一个问题:手势和按钮的点击事件冲突了。你想想看,UIButton 本身就有点击响应,你再给它加个手势,有时候手势先响应了,按钮反而没反应。
gestureRecognizer:shouldReceiveTouch: 代理方法里排除按钮区域。
配置时还有几个关键属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| numberOfTapsRequired | NSInteger | 点击次数,默认 1 |
| numberOfTouchesRequired | NSInteger | 手指数量,默认 1 |
| cancelsTouchesInView | BOOL | 是否取消视图的触摸事件,默认 YES |
cancelsTouchesInView 设为 NO。这样手势识别后,视图还能继续收到触摸事件。
3.2 多击识别:双击、三击怎么配?
双击手势其实很简单——把 numberOfTapsRequired 改成 2 就行。但有个细节:双击和单击同时存在时,系统怎么区分?
举个例子,你想让用户单击选中图片,双击放大图片。如果只加两个手势,你会发现单击总是先触发。为什么会这样?因为手势识别有个延迟机制——系统要等一会儿,确认用户不会点第二下,才触发单击。
// 单击手势
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(singleTap:)];
singleTap.numberOfTapsRequired = 1;
// 双击手势
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(doubleTap:)];
doubleTap.numberOfTapsRequired = 2;
// 关键:单击需要等待双击失败
[singleTap requireGestureRecognizerToFail:doubleTap];
[self.view addGestureRecognizer:singleTap];
[self.view addGestureRecognizer:doubleTap];
requireGestureRecognizerToFail: 是解决多击冲突的关键。它告诉系统:单击手势要等双击手势失败后,才能触发。这样用户双击时,单击就不会乱跳出来了。
我记得在做一个相册 App 时,用户反馈说「双击放大后,单击选中的逻辑乱了」。排查后发现是忘了加 requireGestureRecognizerToFail。加上后,体验立马正常了。
3.3 多指识别:三指同时点击怎么玩?
多指点击在游戏或特殊交互里比较常见。比如某些 App 的「开发者模式」需要三指同时点击才能进入。
UITapGestureRecognizer *threeFingerTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(threeFingerTap:)];
threeFingerTap.numberOfTapsRequired = 1;
threeFingerTap.numberOfTouchesRequired = 3; // 三根手指
[self.view addGestureRecognizer:threeFingerTap];
这里有个坑:模拟器上怎么测试多指? 按住 Option 键,会出现两个触摸点。再按住 Shift 键可以移动它们。但三指就有点麻烦了——我一般直接用真机测。
3.4 手势代理:精细控制触摸行为
有时候你需要更精细的控制。比如,某个区域不允许手势响应,或者手势和其他手势共存。这时候就要用 UIGestureRecognizerDelegate。
// 代理方法:是否允许手势开始
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
// 获取触摸点
CGPoint point = [gestureRecognizer locationInView:self.view];
// 如果触摸点在某个按钮区域内,拒绝手势
if (CGRectContainsPoint(self.specialButton.frame, point)) {
return NO;
}
return YES;
}
// 代理方法:是否允许同时识别多个手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES; // 允许同时响应
}
shouldRecognizeSimultaneouslyWithGestureRecognizer: 返回 YES。不然手势会吃掉滚动事件。
3.5 状态机与点击手势
每个手势识别器都有状态机。点击手势的状态变化很简单:
- Possible:初始状态,等待识别
- Recognized:成功识别为点击
- Failed:识别失败(比如手指移动了)
你可以在回调里通过 gestureRecognizer.state 来判断当前状态。不过说实话,点击手势一般只用 UIGestureRecognizerStateRecognized 就够了。
嗯,这里要注意:点击手势不会进入 Began、Changed、Ended 这些连续状态。它只有「识别成功」和「识别失败」两种结果。如果你需要跟踪手指按下和抬起的细节,得用 UILongPressGestureRecognizer 或者直接重写 touchesBegan: 方法。
3.6 实战:一个完整的点击手势示例
最后,给一个我在项目中常用的封装。它支持单击、双击、三指点击,并且处理了冲突:
- (void)setupTapGestures {
// 单击
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleSingleTap:)];
singleTap.numberOfTapsRequired = 1;
// 双击
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleDoubleTap:)];
doubleTap.numberOfTapsRequired = 2;
// 三指单击
UITapGestureRecognizer *threeFingerTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleThreeFingerTap:)];
threeFingerTap.numberOfTouchesRequired = 3;
// 设置依赖关系
[singleTap requireGestureRecognizerToFail:doubleTap];
// 设置代理
singleTap.delegate = self;
doubleTap.delegate = self;
threeFingerTap.delegate = self;
// 添加到视图
[self.view addGestureRecognizer:singleTap];
[self.view addGestureRecognizer:doubleTap];
[self.view addGestureRecognizer:threeFingerTap];
}
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldReceiveTouch:(UITouch *)touch {
// 排除按钮区域
if ([touch.view isKindOfClass:[UIButton class]]) {
return NO;
}
return YES;
}
这个封装我用了好几年,基本没出过问题。你想想看,把冲突处理、代理设置、依赖关系都放在一起,维护起来多省心。
requireGestureRecognizerToFail: 一定要加上。
好了,这一章就到这里。下一章我们会聊长按手势,那个比点击手势复杂多了——有状态变化、有触发时长、还有移动距离的判断。到时候我再分享几个实战踩坑经历。