深入了解iOS的触摸事件机制(上)

深入了解iOS的触摸事件机制(上)

IOS小彩虹2021-08-22 15:35:17220A+A-

深入了解iOS的触摸事件机制

iOS事件机制是iOS人机交互的重要组成部分. 本文将沿着iOS事件机制发展的历史, 剖析其中细节. 进而全局理解事件机制的运作方式. 过程中, 我们也会尝试去发觉苹果背后的设计理念和思维方式.

苹果在用户体验上细致入微. 在整个人机交互系统中. 苹果设计了很多精巧的方法, 也定义很多Rules. 最终使得iOS的用户体验足够优秀. 同时, 开发者也能方便使用这些特性, 并取得一致性的体验.

iOS触摸事件的状态机

我们先来看单根手指滑动的触摸流程:

经历的阶段:

UITouch状态机

Began: 代表手指的移入

Moved: 手指移动

Ended: 手指离开

Cancelled: 被外部或内部中断.

Stationary: 手指停留在屏幕上, 没有任何移动. 这个状态系统不会通知给上层应用, 我们一般也不需关心. 但他的确是状态机的一部分. (上图状态机未画出)

iOS触摸事件模型

我们知道了一个典型触摸流程的状态机, 下面我们了解一下iOS触摸事件模型下都有哪些对象.

  • UITouch
  • UIEvent
  • UIResponder
    • UIWindow
    • UIViewController
    • UIView
    • UIControl
  • UIGestureRecognizer

UITouch

代表一个触摸源, 通常是一根手指.

切记, UITouch并不是字面上理解的一次touch. 而是代表这根手指, 并跟随其整个生命周期. UITouch的状态就是这根手指在不同时刻的状态.

UITouch由硬件捕获. 通常以60HZ的频率进行刷新. 在iPad Air2以后, 开始有120HZ刷新频率的设备. 虽然硬件会以2倍数量捕获UITouch. 但是iOS对此进行了兼容. 当然, 你也可以通过UIEvent的[UIEvent coalescedTouchesForTouch:]方法拿到2个点.

tapCount是UITouch的一个重要属性. 当进行双击时, 硬件会以一定的latency来判断是该上报2个UITouch的移入--移出呢? 还是仅上报一个UITouch, 并标记它的tapCount. 这个属性对判断双击或者多击有着关键作用.

UIEvent

UIEvent指代一个触摸事件.

iOS设计的时候充分考虑的扩展性. UIEvent并不总是触摸事件, 虽然通常情况下都是触摸事件. 本文也只会涉及触摸事件. 详细请看UIEvent对象的type或者subtype枚举.

UIEvent代表这次交互的整个生命周期. 在其过程中, 随时会有就的手指的离开, 或者新的手指的移入. 直到某个时刻全部手指都离开并持续一个latency为止. 这时UIEvent才算结束.

// UIEvent
@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;

通过allTouches属性可以拿到这次交互的所有UITouch对象.

- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;

还可以通过上面2个方法拿到绑定到某个window或者view的所有UITouch对象.

UIEvent与其他对象关系图示:

UIResponder

所有继承自UIResponder的对象都能处理UITouch事件. UIWindow, UIViewController, UIView的树状结构构成了整个Responder Chain.

Responder Chain图示:

事件会从Hit Test View开始, 然后顺着响应者链由下往上(视觉层面是由上往下)进行转发. 直到不再往上传递为止. UIResponder对象的默认行为是往上传递, 如果不想继续传递, 不调用super即可.

UIWindow

如果你的应用包含多个UIWindow. 系统拿到UITouch事件过后. 系统按照UIWindow层级. 逐层向下进行hittest. 直到某个UIWindow返回一个UIView对象.

UIWindow负责解析UIEvent对象, 并把相应的UITouch对象传递给Hit Test View 或者 UIGestureEnvironment.

UIViewController

UITouch事件在顺着Responder Chain向上forward的过程中. 如果某个UIViewController的根view没有响应UITouch. 系统会优先分发给对应的UIViewController. 而不是直接superView. 紧接着再发送给superView.

在UIViewController中重写touch方法可以使得不需要创建UIButton或者借助子类化UIView来实现事件回调. 例如实现点击空白隐藏键盘等.

UIView

最常见的Touch事件处理单元.

对于Drawing类的App来说. 我们需要重写Touch方法. 而其他方法我们很少见

重写HitTest方法和PointInside方法使得你可以巧妙的manipulate Hit Test过程. 从而让Hit Test动态化, 根据App的状态动态返回Hit Test View. 后面我们再详细介绍Hit Test

UIControl

UIControl是一个特殊的UIView. 其实现了诸多用户触摸相关的交互. 并通过Target-Action的设计模式来和你的代码进行交互.

其与UIGestureRecognizer的共存处理也会与普通UIView不同. 后面我们会讲到.

// UIControlEvents

UIControlEventTouchDown
UIControlEventTouchDownRepeat
UIControlEventTouchDragInside
UIControlEventTouchDragOutside
UIControlEventTouchDragEnter
UIControlEventTouchDragExit
UIControlEventTouchUpInside
UIControlEventTouchUpOutside
UIControlEventTouchCancel

以上的事件类型几乎覆盖你需要的所有场景. 因此, 最简单的方法是添加一个UIControl或者UIButton对象, 而不是重写Touch事件.

UIGestureRecognizer

UIGestureRecognizer是本文的重点.

UIGestureRecognizer是苹果在iOS3.2(2009年后期)时首次推出. UIGestureRecognizer是对一系列手指操作的抽象. 是一个强大的工具.

UIGestureRecognizer建立在UITouch之上. 它将具体的Touch Handling封装起来. 使你可以直接使用其抽象出来的点击、Pinch(双指捏合)、Pan(拖动), 而不用自己亲自去处理Touch事件.我们应该尽可能的使用更上层的工具.除非你想要个性化的功能.

iOS触摸事件机制的二段式

在了解基iOS事件模型里一些关键对象的基本概念后. 下面我们看看系统是让这些对象协同运行的.

iOS设备通过硬件得到触摸坐标及状态信息. 然后系统需要讲这些信息准确传递到我们视觉上的物体(视图). 会经历2个过程.

Hit Test

iOS系统通过View的树状结构, 来计算包含触摸点的最顶上的视图.

系统通过[UIView hitTest:withEvent:]来得到点击的视图. 它是一个递归函数, 每个UIView对象都会对自己的subvew进行调用. 直到某个UIView的[UIView pointInside:withEvent:]返回为true, 并且没有子view为止.

伪函数如图:

流程图:

iOS将处理Hit Test流程的2个关键函数暴露出来. 使得我们可以对Hit Test流程进行动态控制.

2个函数的能力不一致, 使得我们可以在不同维度进行控制.

这2个函数也是我们能够定制iOS事件机制的最早时机.

分发消息

通常情况下, iOS会将这些触摸消息分发到对应的view上. 但是, 如果此View不处理. 系统默认也会将其消息通过响应者链逐步往上转发.

通过UIApplication, UIWindow进行分发

// UIApplication
- (void)sendEvent:(UIEvent *)event;

// UIWindow
- (void)sendEvent:(UIEvent *)event;

下面我们通过一张图, 了解UITouch事件分发的具体流程

图中: UIGestureEnvironment是专门负责管理所有UIGesture的类.

多点触摸

multipleTouchEnabled属性控制UIView是否支持多点触摸. 默认为false.

通常情况下我们也不需要同时处理多个手指的Touch事件. 此时, 在整个这次UIEvent周期内, 第一个点击到该View的UITouch会将view属性进行正确标记. 往后所有点击到该view的UITouch, view属性将会被置为nil. 但同时也会附加到UIEvent上.

正因如此, 当你看到anyObject调用时才不会感觉差异. 因为默认情况下, 它都是同一个对象. 如图:

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
  // 你经常会看到别人这样获取UITouch对象, 是不是心生疑虑呢?
  UITouch *touch = [touches anyObject];
  ...
}

如果为true. 这些UITouch都会传递到UIView的Touch事件里.

UIGestureRecognizer

为什么苹果要设计UIGestureRecognizer呢?

一个案例

如果要你实现一个双指捏合缩放并移动的逻辑, 你应该怎么处理呢? 你肯定会相当又会是一堆数据运算. 对的.

嘿嘿, 这里根据2个手指的坐标, 手动计算矩阵的值. 前4个值用于矩阵线性变换, 后2个值用来做偏移. 更多矩阵变换相关知识, 大家可以参考高数.

而且, 这里仅仅是针对最多2个手指进行处理. 如果有3个或者更多. 代码还得改进.

以上代码大部分源于WWDC2009 中的MultiTouchDemo. 查看完整Demo

事实上在iOS3.2之前, UIScrollView的缩放就是这样实现的.并没有使用UIGestureRecognizer.

苹果推出UIGestureRecognizer的目的就是对这些数学运算进行封装, 使得开发者更方便的对用户的触摸进行转换, 同时也避免了开发者不一致的实现导致体验上的差异.

(未完待续...)

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示 | 支付宝红包
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们