手势识别器入门: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 等)是并行存在的。它们之间通过一套「协商机制」来决定谁先处理事件。

具体来说:

  1. 当触摸发生时,系统会同时把事件传递给 View 的触摸方法和手势识别器。
  2. 手势识别器会先于 View 收到触摸事件(但不会立即处理)。
  3. 手势识别器通过状态机判断是否识别成功。
  4. 如果识别成功,系统会取消 View 的触摸事件(调用 touchesCancelled)。
  5. 如果识别失败,触摸事件会正常传递给 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 的架构和协作关系。核心要点就三个:

  • 手势识别器是一个状态机驱动的「手势翻译器」
  • 它和触摸事件是并行关系,通过协商机制决定谁先处理
  • 代理方法提供了精细控制,可以解决大部分冲突场景

下一章,我们会深入每个具体手势的实现细节。到时候我会分享一些我在实际项目中踩过的坑——比如为什么双击手势总是触发单击手势,以及怎么优雅地解决它。

好,今天就到这里。有什么问题,欢迎随时交流。