手势识别器入门:UIGestureRecognizer 架构与触摸事件协作
好,咱们今天来聊聊手势识别器。说实话,我刚入行那会儿,对手势的理解就是「点一下、滑一下」。直到有一次做一款绘图 App,用户反馈说「画线时偶尔会触发返回手势」——我才意识到,手势识别器背后的门道,远比想象中深。
这一章,我会带你从架构层面拆解 UIGestureRecognizer。说白了,就是搞清楚它是什么、怎么工作、以及和触摸事件之间到底怎么「配合演戏」。
1. UIGestureRecognizer 是什么?
UIGestureRecognizer 是 iOS 提供的一个抽象基类。它的核心任务,就是把原始的触摸事件(UITouch)翻译成我们熟悉的手势——比如点击、滑动、捏合、旋转。
你想想看,如果没有它,我们得自己去维护触摸点的状态机:
- 触摸开始了吗?
- 移动了多少距离?
- 速度够不够快?
- 是不是多点触控?
嗯,这些脏活累活,UIGestureRecognizer 全帮你干了。我个人习惯把它看作一个「手势翻译器」——输入是触摸事件流,输出是手势状态变化。
2. 架构概览:类层次结构
UIGestureRecognizer 本身不直接处理具体手势。它定义了一套通用的状态机机制和生命周期。具体的识别逻辑,交给它的子类去实现。
系统内置了这些常用子类:
| 手势类型 | 类名 | 典型场景 |
|---|---|---|
| 点击 | UITapGestureRecognizer | 按钮点击、双击放大 |
| 滑动 | UISwipeGestureRecognizer | 列表左滑删除 |
| 拖拽 | UIPanGestureRecognizer | 拖动视图、滚动 |
| 捏合 | UIPinchGestureRecognizer | 图片缩放 |
| 旋转 | UIRotationGestureRecognizer | 图片旋转 |
| 长按 | UILongPressGestureRecognizer | 唤起菜单、拖拽排序 |
| 边缘滑动 | UIScreenEdgePanGestureRecognizer | 返回手势 |
这些子类都继承自 UIGestureRecognizer。你完全可以自己写一个子类,实现自定义手势——比如画个五角星触发某个操作。我在项目中就做过一个「画圈触发刷新」的手势,嗯,效果还挺酷的。
3. 核心机制:状态机
UIGestureRecognizer 内部维护了一个状态机。这是它最核心的设计。状态机定义了手势从开始到结束的完整生命周期。
状态包括:
- Possible:初始状态。手势还没开始,但随时可能开始。
- Began:手势已识别,开始执行。
- Changed:手势正在变化(比如手指在移动)。
- Ended:手势正常结束。
- Cancelled:手势被系统中断(比如来电、系统弹窗)。
- Failed:手势识别失败。
为什么会设计这么复杂的状态机?
因为手势识别不是一锤子买卖。比如一个拖拽手势,手指可能先放下一段时间,然后才开始移动。系统需要在这期间不断判断:这到底是一个长按,还是一个拖拽?
状态机让这个过程变得可预测、可管理。每个手势子类只需要关注自己关心的状态转换即可。
重要提示:手势识别器在状态变为 Began 之前,会一直处于 Possible 状态。这意味着它可能在「观望」——等待更多触摸事件来判断是否匹配自己的手势模式。
4. 与触摸事件的协作关系
这是很多开发者容易忽略的地方。UIGestureRecognizer 和 UIResponder 的触摸事件(touchesBegan/touchesMoved 等)是并行存在的。它们之间通过一套「协商机制」来决定谁先处理事件。
具体来说:
- 当触摸发生时,系统会同时把事件传递给 View 的触摸方法和手势识别器。
- 手势识别器会先于 View 收到触摸事件(但不会立即处理)。
- 手势识别器通过状态机判断是否识别成功。
- 如果识别成功,系统会取消 View 的触摸事件(调用 touchesCancelled)。
- 如果识别失败,触摸事件会正常传递给 View。
你想想看,这意味着什么?
意味着如果你在 View 里重写了 touchesBegan,同时又添加了一个手势识别器——那么你的 touchesBegan 可能会被「半路截胡」。我在项目中遇到过这种情况:一个自定义绘图 View 里,用户画线时偶尔会触发双击手势,导致画到一半的线条被取消。排查了半天,才发现是手势识别器在作怪。
避坑指南:如果你希望手势识别器和触摸事件共存,可以设置手势识别器的 cancelsTouchesInView 属性为 NO。这样手势识别成功后,不会取消 View 的触摸事件。
5. 手势识别器的生命周期
一个典型的手势识别流程是这样的:
- 用户触摸屏幕 → 系统生成 UITouch 对象
- 系统将触摸事件传递给手势识别器(通过 hit-test 找到对应的 View)
- 手势识别器调用 touchesBegan:withEvent: 等方法
- 手势识别器内部更新状态机
- 状态变为 Began 时,触发对应的 Action 方法
- 状态变为 Changed 时,持续触发 Action
- 状态变为 Ended 或 Cancelled 时,手势结束
这里有个细节:手势识别器并不是直接调用你的 Action 方法。它通过 target-action 机制,把状态变化通知给你。你可以在初始化时指定 target 和 action:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleTap:)];
[self.view addGestureRecognizer:tap];
当手势状态变化时,系统会调用 handleTap: 方法。你可以在方法里通过 gestureRecognizer.state 来判断当前状态。
6. 手势识别器的代理方法
UIGestureRecognizerDelegate 提供了一些精细控制的方法。我个人觉得最常用的是这几个:
- gestureRecognizerShouldBegin: 手势即将开始时调用。返回 NO 可以阻止手势开始。
- gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: 是否允许同时识别多个手势。
- gestureRecognizer:shouldReceiveTouch: 是否接收某个触摸事件。
举个例子。我在做一个可拖拽的卡片视图时,希望卡片既能响应点击,又能响应拖拽。默认情况下,点击手势和拖拽手势会互相干扰。通过代理方法,我让它们可以同时识别:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
嗯,这样用户点击卡片时不会影响拖拽,拖拽时也不会误触点击事件。
注意:同时识别多个手势时,要小心处理状态冲突。比如一个手势在 Began 状态,另一个手势在 Ended 状态,可能会导致 UI 表现不一致。建议在代理方法里加上具体的判断逻辑,而不是无脑返回 YES。
7. 手势识别器与响应链
手势识别器是附加在 View 上的。但它并不属于响应链的一部分。它更像是一个「观察者」——监听触摸事件,然后决定是否拦截。
当一个 View 上有多个手势识别器时,系统会按照添加顺序来处理。但如果有嵌套的 View(比如父 View 和子 View 都有手势),情况就复杂了。
系统默认的处理规则是:
- 子 View 的手势识别器优先于父 View 的
- 如果子 View 的手势识别失败,父 View 的才有机会
- 如果子 View 的手势识别成功,父 View 的会被取消
我曾经做过一个需求:在一个滚动视图里嵌入一个可拖拽的子视图。默认情况下,子视图的拖拽手势会阻止滚动视图的滑动。解决办法是让子视图的拖拽手势在特定条件下失败,或者通过代理方法协调两者关系。
8. 小结
这一章我们聊了 UIGestureRecognizer 的架构和协作关系。核心要点就三个:
- 手势识别器是一个状态机驱动的「手势翻译器」
- 它和触摸事件是并行关系,通过协商机制决定谁先处理
- 代理方法提供了精细控制,可以解决大部分冲突场景
下一章,我们会深入每个具体手势的实现细节。到时候我会分享一些我在实际项目中踩过的坑——比如为什么双击手势总是触发单击手势,以及怎么优雅地解决它。
好,今天就到这里。有什么问题,欢迎随时交流。