[iOS] 触摸事件的处理

1. 事件的生命周期

当触碰屏幕的那一刻,一个触摸事件就在系统中生成了。经过IPC进程间通信,事件最终被传递到了合适的应用。在应用内历经峰回路转的奇幻之旅后,最终被释放。大致经过如下图:

image.png
1.1 系统响应阶段
  • 手指触摸屏幕,屏幕感应到触碰后,将事件交由IOKit处理

IOKit.framework 是与硬件或内核服务通信的低级框架。

  • IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoard进程

SpringBoard.app 是 iOS 和 iPadOS 负责管理主屏幕的基础程序,并在设备启动时启动 WindowServer、开启应用程序(实现该功能等程序称为应用启动器)和对设备进行某些设置。有时候主屏幕也被作为 SpringBoard 的代称。主要处理按键(锁屏/静音等)、触摸、加速、距离传感器等几种事件,随后通过mac port进程间通信转发至需要的APP。 Mac OSX中使用的是Launchpad,能让用户以从类似于iOS的SpringBoard的界面按一下图示来启动应用程式。在启动台推出之前,用户能以Dock、Finder、Spotlight或终端启动应用。不过 Launchpad 并不会占据整个主屏幕,而更像是一个 Space(类似于仪表板)。

  • SpringBoard进程因接收到触摸事件,触发了主线程runloopsource事件源的回调。

此时SpringBoard会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。
若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;
若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。

1.2 APP响应阶段
  • APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调;
  • source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应;
  • source0回调内部将触摸事件添加到UIApplication对象的事件队列中。事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing。另外,此处开始便是与我们平时开发相关的工作了;
  • 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了。事实上,事件除了被响应者消耗,还能被手势识别器或是target-action模式捕捉并消耗掉。其中涉及对触摸事件的响应优先级;
  • 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。

2. 触摸、事件、响应者

这里先对这三者进行简单了解和记录。

2.1 触摸(UITouch)

表示屏幕上发生的触摸的位置(location)、大小(size)、移动(movement)和力度(force,针对 3D Touch 和 Apple Pencil)的对象。

  • 一个手指一次触摸屏幕,就对应生成一个UITouch对象,多个手指同时触摸,就会生成多个UITouch对象
  • 多个手指先后触摸,系统会根据触摸的位置和时间,判断是否更新同一个UITouch对象。如两个手指一前一后触摸同一个位置(双击),那么第一次触摸时生成一个UITouch对象,第二次触摸更新这个UITouch对象(UITouch对象的tap count属性值从1变成2);如果两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者之间没有联系
  • 每个UITouch对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息
  • 手指离开屏幕一段时间后,确定该UITouch对象不会再被更新将被释放

打印一个 UITouch 对象的可看到如下内容:

 
phase: Moved 
tap count: 1 
force: 0.000 

window: ; layer = > 
view: > 

location in window: {219.33332824707031, 428.66665649414062} 
previous location in window: {220, 429} 
location in view: {106.66666158040363, 97.666656494140625} 
previous location in view: {107.33333333333331, 98}

可以通过传递给响应者对象(UIResponder 或其子类)的 UIEvent 对象访问 touch 对象(allTouches 属性),以进行事件处理。touch 对象包括用于以下对象的访问:

  • 发生触摸的 view 或 window
  • 触摸在 view 或 window 中的位置
  • 触摸的近似半径(approximate radius)
  • 触摸的力度(force)(在支持 3D Touch 或 Apple Pencil 的设备上)

touchgestureRecognizers 属性包含当前正在处理touchgesture recognizers。每个gesture recognizer都是UIGestureRecognizer 的具体子类的实例。

具体属性如下:

/ 触摸发生的时间
@property(nonatomic,readonly) NSTimeInterval      timestamp; 
/ 触摸的阶段
@property(nonatomic,readonly) UITouchPhase        phase;
/ 触摸的次数
@property(nonatomic,readonly) NSUInteger          tapCount; 

/ 触摸的类型: UITouchTypeDirect(直接触碰) UITouchTypeIndirect(间接触碰) UITouchTypeStylus(触笔触碰)
@property(nonatomic,readonly) UITouchType         type API_AVAILABLE(ios(9.0)); 

/ 触摸的半径
@property(nonatomic,readonly) CGFloat majorRadius API_AVAILABLE(ios(8.0));  
/ 触摸半径的公差
@property(nonatomic,readonly) CGFloat majorRadiusTolerance API_AVAILABLE(ios(8.0)); 
/ 触摸的原始窗口
@property(nonatomic,readonly,strong) UIWindow  *window; 
/ 触摸的视图
@property(nonatomic,readonly,strong) UIView  *view; 
/ 手势识别器
@property(nonatomic,readonly,copy)   NSArray  *gestureRecognizers; 

/ 返回坐标系中的当前位置
- (CGPoint)locationInView:(nullable UIView *)view;
/ 返回坐标系中的先前位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;

/ 触摸可用时,返回触摸的精确位置,不要用于hit-test中
- (CGPoint)preciseLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
- (CGPoint)precisePreviousLocationInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));

/ 触摸的力度,其中1的值代表平均触摸的力(由系统预先确定,而不是用户特定)
@property(nonatomic,readonly) CGFloat force API_AVAILABLE(ios(9.0));
/ 触碰的最大可能力
@property(nonatomic,readonly) CGFloat maximumPossibleForce API_AVAILABLE(ios(9.0));

/ 返回手触笔的方位角(弧度)
- (CGFloat)azimuthAngleInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));
/ 返回指向指针的方位角的单位向量
- (CGVector)azimuthUnitVectorInView:(nullable UIView *)view API_AVAILABLE(ios(9.1));

/ 触笔和屏幕的弧度
@property(nonatomic,readonly) CGFloat altitudeAngle API_AVAILABLE(ios(9.1));

/ 一个索引号码,让你把一个更新的触摸与原来的触摸联系起来。
@property(nonatomic,readonly) NSNumber * _Nullable estimationUpdateIndex API_AVAILABLE(ios(9.1));

/ 一组触控属性,其值仅包含估计值
@property(nonatomic,readonly) UITouchProperties estimatedProperties API_AVAILABLE(ios(9.1));

/ 未来预期更新值的触控属性集
@property(nonatomic,readonly) UITouchProperties estimatedPropertiesExpectingUpdates API_AVAILABLE(ios(9.1));

2.2 事件 (UIEvent)
  • 触摸的目的是生成触摸事件供响应者响应,一个触摸事件对应一个UIEvent对象,其中的type属性标识了事件的类型(有触摸事件、摇晃事件等)
  • 一个UIEvent不只有一个触摸对象,因为一个触摸事件可能是由多个手指同时触摸产生的
    可以使用类型(type)和子类型(subtype)属性确定事件的类型。
/ 事件类型
typedef NS_ENUM(NSInteger, UIEventType) {
    UIEventTypeTouches, / 触摸事件
    UIEventTypeMotion, / 摇晃事件
    UIEventTypeRemoteControl, / 遥控事件类型
    UIEventTypePresses API_AVAILABLE(ios(9.0)), / 物理按钮事件类型 
    UIEventTypeScroll      API_AVAILABLE(ios(13.4), tvos(13.4))  API_UNAVAILABLE(watchos) = 10,
    UIEventTypeHover       API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 11,
    UIEventTypeTransform   API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos) = 14,
};

/ 子类型
typedef NS_ENUM(NSInteger, UIEventSubtype) {
    / 事件没有子类型 iOS3.0之后可以用
    UIEventSubtypeNone                              = 0,
    
    / 事件子类型晃动的设备 iOS3.0之后可以用
    UIEventSubtypeMotionShake                       = 1,
    
    / 遥控的事件子类型 iOS4.0之后可以用
    UIEventSubtypeRemoteControlPlay                 = 100,//播放
    UIEventSubtypeRemoteControlPause                = 101,//暂停
    UIEventSubtypeRemoteControlStop                 = 102,//停止
    UIEventSubtypeRemoteControlTogglePlayPause      = 103,//播放和暂停之间切换【操作:播放或暂停状态下,按耳机线控中间按钮一下】
    UIEventSubtypeRemoteControlNextTrack            = 104,//下一曲【操作:按耳机线控中间按钮两下】
    UIEventSubtypeRemoteControlPreviousTrack        = 105,//上一曲【操作:按耳机线控中间按钮三下】
    UIEventSubtypeRemoteControlBeginSeekingBackward = 106,//快退开始【操作:按耳机线控中间按钮三下不要松开】
    UIEventSubtypeRemoteControlEndSeekingBackward   = 107,//快退结束【操作:按耳机线控中间按钮三下到了快退的位置松开】
    UIEventSubtypeRemoteControlBeginSeekingForward  = 108,//快进开始【操作:按耳机线控中间按钮两下不要松开】
    UIEventSubtypeRemoteControlEndSeekingForward    = 109,//快进结束【操作:按耳机线控中间按钮两下到了快进的位置松开】
};


@interface UIEvent : NSObject

/ 事件类型
@property(nonatomic,readonly) UIEventType     type API_AVAILABLE(ios(3.0));
/ 事件子类型
@property(nonatomic,readonly) UIEventSubtype  subtype API_AVAILABLE(ios(3.0));
/ 事件发生时间戳
@property(nonatomic,readonly) NSTimeInterval  timestamp;
/ 
@property (nonatomic, readonly) UIKeyModifierFlags modifierFlags API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchOS);
@property (nonatomic, readonly) UIEventButtonMask buttonMask API_AVAILABLE(ios(13.4)) API_UNAVAILABLE(tvos, watchOS);

/ 返回与事件关联的所有 touches(alltouchs 属性只包含触摸序列中的最后一次触摸)
@property(nonatomic, readonly, nullable) NSSet  *allTouches;
/ 从 UIEvent 中返回属于指定 window 的 UITouch 对象。
- (nullable NSSet  *)touchesForWindow:(UIWindow *)window;
/ 从 UIEvent 中返回属于指定 view 的 UITouch 对象。
- (nullable NSSet  *)touchesForView:(UIView *)view;
/ 返回要传递到指定 gesture recognizer 的 UITouch 对象
- (nullable NSSet  *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture API_AVAILABLE(ios(3.2));

/ 会将丢失的触摸放到一个新的 UIEvent 数组中,你可以用 coalescedTouchesForTouch(_:) 方法来访问
- (nullable NSArray  *)coalescedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));

/ 辅助UITouch的触摸,预测发生了一系列主要的触摸事件。这些预测可能不完全匹配的触摸的真正的行为,因为它的移动,所以他们应该被解释为一个估计。
- (nullable NSArray  *)predictedTouchesForTouch:(UITouch *)touch API_AVAILABLE(ios(9.0));

@end
2.3 响应者 (UIResponder)

每个响应者都是一个UIResponder对象,即所有继承或者间接继承自UIResponder的对象,本身都具备响应事件的能力。因此以下类的实例都是响应者:

  • UIView
  • UIViewController
  • UIApplication
  • AppDelegate
@interface UIResponder : NSObject 
/ 下一个响应者,如果没有,则为nil
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
/ 是否可以成为第一响应者,默认为NO
@property(nonatomic, readonly) BOOL canBecomeFirstResponder;   
- (BOOL)becomeFirstResponder;
/ 是否可以取消第一响应者,默认为YES
@property(nonatomic, readonly) BOOL canResignFirstResponder;  
/ 取消第一响应者
- (BOOL)resignFirstResponder;
/ 是否是第一响应者
@property(nonatomic, readonly) BOOL isFirstResponder;

/ 手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
/ 手指在屏幕上移动
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
/ 手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
/ 触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
/ 3D 触摸事件
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches API_AVAILABLE(ios(9.1));

// Generally, all responders which do custom press handling should override all four of these methods.
// Your responder will receive either pressesEnded:withEvent or pressesCancelled:withEvent: for each
// press it is handling (those presses it received in pressesBegan:withEvent:).
// pressesChanged:withEvent: will be invoked for presses that provide an analog value
// (like thumbsticks or analog push buttons)
// *** You must handle cancelled presses to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));

/ 开始运动
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
/ 结束运动
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
/ 取消运动
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));

/ 接收到远程控制事件时通知对象
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event API_AVAILABLE(ios(4.0));

- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender API_AVAILABLE(ios(3.0));
// Allows an action to be forwarded to another target. By default checks -canPerformAction:withSender: to either return self, or go up the responder chain.
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender API_AVAILABLE(ios(7.0));

// Overrides for menu building and validation
- (void)buildMenuWithBuilder:(id)builder API_AVAILABLE(ios(13.0));
- (void)validateCommand:(UICommand *)command API_AVAILABLE(ios(13.0));

@property(nullable, nonatomic,readonly) NSUndoManager *undoManager API_AVAILABLE(ios(3.0));

// Productivity editing interaction support for undo/redo/cut/copy/paste gestures
@property (nonatomic, readonly) UIEditingInteractionConfiguration editingInteractionConfiguration API_AVAILABLE(ios(13.0));

@end

响应者之所以能够响应事件,因为其提供了4个处理触摸事件的方法:

//手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移动
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
//手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;

这几个方法在响应者对象接收到事件的时候调用,用于做出对事件的响应。

3. 寻找事件的最佳响应者 (hit-testing)

上面说过APP接收到触摸事件后,会被放入当前应用的一个事件队列中(先进先出)。

每个事件的理想宿命是能够被响应它的对象响应后释放,然而响应者诸多,事件一次只有一个,为避免纷争,就得有一个先后顺序,也就是得有一个响应者的优先级。因此,这就存在一个寻找事件最佳响应者(又称第一响应者 first responder)的过程,目的是找到一个具备最高优先级响应权的响应对象(the most appropriate responder object),这个过程叫做Hit-Testing,那个命中的最佳响应者称为hit-tested view

找到这个最佳响应者之后,就会调用视图控件的touches方法来做具体的事件处理。

3.1 事件自下而上的传递

应用接收到事件后先将其置入事件队列中,出队后,application首先将事件传递给当前应用最后显示的窗口(UIWindow)询问其能否响应事件。若窗口能响应事件,则传递给子视图询问是否能响应,子视图若能响应则继续询问子视图。事件传递顺序如下:

UIApplication ——> UIWindow ——> 子视图 ——> ... ——> 子视图

子视图询问的顺序是优先询问后添加的子视图,即子视图数组中靠后的视图。
把UIWindow也看成是视图即可,这样整个传递过程就是一个递归询问子视图能否响应事件过程,且后添加的子视图优先级高(对于window而言就是后显示的window优先级高)。

3.1.1 具体流程

使用UIView的hitTest:withEvent:方法遍历视图层次结构,寻找包含指定触摸的最深的子视图,它成为触摸事件的第一个响应器。

  1. UIApplication首先将事件传递给窗口对象(UIWindow),若存在多个窗口,则优先询问后显示的窗口
  2. 若窗口不能响应事件,则将事件传递其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图。
  3. 重复步骤2。即视图若不能响应,则将事件传递给上一个同级子视图;若能响应,则从后往前询问当前视图的子视图。
  4. 视图若没有能响应的子视图了,则自身就是最合适的响应者。
3.1.2 查找示例
image.png

视图层级如下(同一层级的视图越在下面,表示越后添加):

A
├── B
│   └── D
└── C
    ├── E
    └── F

现在假设在E视图所处的屏幕位置触发一个触摸,应用接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:

image.png

  1. UIWindow将事件传递给其子视图A
  2. A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。
  3. C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。
  4. F判断自身不能响应事件,C又将事件传递给E。
  5. E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者。
3.1.3 hit-testing

hitTest:withEvent:会调用每个子视图的pointInside:withEvent:方法来遍历视图层次结构,以确定哪个子视图应该接收触摸事件。如果pointInside:withEvent:返回YES,则遍历子视图的层次结构,直到找到包含指定点的最前面的视图为止。

其次,下面三种情况的视图无法响应事件:

  • hidden 为YES
  • 用户交互关闭 userInteractionEnabled = NO
  • alpha 小于0.01

hitTest:withEvent: 方法,这个方法是Hit-Testing过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁。

hitTest:withEvent:方法返回一个UIView对象,作为当前视图层次中的响应者。默认实现是:

  • 若当前视图无法响应事件,则返回nil
  • 若当前视图可以响应事件,但无子视图可以响应事件,则返回自身作为当前视图层次中的事件响应者
  • 若当前视图可以响应事件,同时有子视图可以响应,则返回子视图层次中的事件响应者

一开始UIApplication将事件通过调用UIWindow对象的 hitTest:withEvent:传递给UIWindow对象,UIWindowhitTest:withEvent:在执行时若判断本身能响应事件,则调用子视图的hitTest:withEvent: 将事件传递给子视图并询问子视图上的最佳响应者。最终UIWindow返回一个视图层次中的响应者视图给UIApplication,这个视图就是hit-testing的最佳响应者。

系统对于视图能否响应事件的判断逻辑除了之前提到的3种限制状态,默认能响应的条件就是触摸点在当前视图的坐标系范围内。因此,hitTest:withEvent: 的默认实现就可以推测了,大致如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    //3种状态无法响应事件
     if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil; 
    //触摸点若不在当前视图上则无法响应事件
    if ([self pointInside:point withEvent:event] == NO) return nil; 
    //从后往前遍历子视图数组 
    int count = (int)self.subviews.count; 
    for (int i = count - 1; i >= 0; i--) 
    { 
        // 获取子视图
        UIView *childView = self.subviews[I]; 
        // 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
        CGPoint childP = [self convertPoint:point toView:childView]; 
        //询问子视图层级中的最佳响应视图
        UIView *fitView = [childView hitTest:childP withEvent:event]; 
        if (fitView) 
        {
            //如果子视图中有更合适的就返回
            return fitView; 
        }
    } 
    //没有在子视图中找到更合适的响应视图,那么自身就是最合适的
    return self;
}

值得注意的是 pointInside:withEvent: 这个方法,用于判断触摸点是否在自身坐标范围内。默认实现是若在坐标范围内则返回YES,否则返回NO。

我们如果自定义UIView去重写hitTest:withEvent:方法,会发现这个方法执行两遍,目前找到合理的解释是:https://lists.apple.com/archives/cocoa-dev/2014/Feb/msg00118.html
The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine. (系统可能会调整touch point,所以会多次调用hit test).

3.2 事件拦截
  • 正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view
  • 不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view
  • 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件

事件传递给谁,就会调用谁的hitTest:withEvent:方法
注意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
技巧:想让谁成为最合适的view就重写谁的父控件的hitTest:withEvent:方法,返回指定的子控件,或者重写自己的hitTest:withEvent:方法return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view

3.2.1 练习

屏幕上现在有一个viewA,viewA有一个subView叫做viewB,要求触摸viewB时,viewB会响应事件,而触摸viewA本身,不会响应该事件。如何实现?

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ 
  UIView *view = [super hitTest:point withEvent:event]; 
  if (view == self) { 
    return nil; 
  } 
    return view; 
  }
3.3 事件的响应

经过hit-testing之后,就已经知道事件的最佳响应者是谁了,那么接下来要做的就是:

  • 将事件传递给最佳响应者响应
  • 事件沿着响应链传递
3.3.1 事件响应的前奏

因为最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。首先,UIApplication将事件通过 sendEvent:传递给事件所属的windowwindow同样通过 sendEvent: 再将事件传递给hit-tested view,即最佳响应者。过程如下:

UIApplication ——> UIWindow ——> hit-tested view

这个过程中,假如应用中存在多个window对象,UIApplication是怎么知道要把事件传给哪个window的?window又是怎么知道哪个视图才是最佳响应者的呢?

其实简单思考一下,这两个过程都是传递事件的过程,涉及的方法都是 sendEvent: ,而该方法的参数(UIEvent对象)是唯一贯穿整个经过的线索,那么就可以大胆猜测必然是该触摸事件对象上绑定了这些信息。事实上之前在介绍UITouch的时候就说过touch对象保存了触摸所属的windowview,而event对象又绑定了touch对象,如此一来,是不是就说得通了

image.png

至于这两个属性是什么时候绑定到touch对象上的,必然是在hit-testing的过程中啊。

3.3.2 事件的响应

前面介绍UIResponder的时候说过,每个响应者必定都是UIResponder对象,通过4个响应触摸事件的方法来响应事件。每个UIResponder对象默认都已经实现了这4个方法,但是默认不对事件做任何处理,单纯只是将事件沿着响应链传递。若要截获事件进行自定义的响应操作,就要重写相关的方法。例如,通过重写 touchesMoved: withEvent: 方法实现简单的视图拖动:

//MovedView
//重写touchesMoved方法(触摸滑动过程中持续调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    //获取触摸对象
    UITouch *touch = [touches anyObject];
    //获取前一个触摸点位置
    CGPoint prePoint = [touch previousLocationInView:self];
    //获取当前触摸点位置
    CGPoint curPoint = [touch locationInView:self];
    //计算偏移量
    CGFloat offsetX = curPoint.x - prePoint.x;
    CGFloat offsetY = curPoint.y - prePoint.y;
    //相对之前的位置偏移视图
    self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

每个响应者都有权决定是否执行对事件的响应,只要重写相关的触摸事件方法即可。

3.3.3 事件的响应 (响应链)

前面一直在提最佳响应者,之所以称之为“最佳”,是因为其具备响应事件的最高优先权(站在响应链顶端)。最佳响应者首先接收到事件,然后便拥有了对事件的绝对控制权:即它可以选择独吞这个事件,也可以将这个事件往下传递给其他响应者,这个由响应者构成的视图链就称之为响应链。

需要注意的是,上一节中也说到了事件的传递,与此处所说的事件的响应有本质区别。上一节所说的事件传递的目的是为了寻找事件的最佳响应者,是自下而上的传递;而这里的事件响应目的是响应者做出对事件的响应,这个过程是自上而下的。前者为“寻找”,后者为“响应”。

响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent: 方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。

响应者对于接收到的事件有3种操作:

  1. 不拦截,默认操作
    事件会自动沿着默认的响应链往下传递
  2. 拦截,不再往下分发事件
    重写 touchesBegan:withEvent: 进行事件处理,不调用父类的 touchesBegan:withEvent:
  3. 拦截,继续往下分发事件
    重写touchesBegan:withEvent: 进行事件处理,同时调用父类的 touchesBegan:withEvent: 将事件往下传递
3.3.4 如何判断下一个响应者?
  1. 如果当前这个view是控制器的view,那么控制器就是下一个响应者
  2. 如果当前这个view不是控制器的view,那么父控件就是下一个响应者
    view.nextResponder

响应者链的事件传递过程:

  1. 如果view是视图控制器的根视图,则下一个响应者是UIViewController,否则就是父视图。
  2. 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
  3. 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
  4. UIApplication对象,下一个responderUIApplicationDelegate,但前提是UIApplicationDelegateUIResponder的一个实例而不是视图,视图控制器,或app对象本身。
3.3.5 改变响应者链

可以通过覆盖responder对象的nextResponder属性来改变响应者链,这样的话,下一个responder就是你返回的对象。

许多UIKit类都已经覆盖了这个属性并返回特定的对象,比如UIView。如果视图是视图控制器的根视图,则下一个响应者是UIViewController,否则就是父视图。

你可能感兴趣的:([iOS] 触摸事件的处理)