iOS触摸事件详解

目录:
1 UITouch
1.1 UITouch的创建
1.2 UITouch的作用
1.3 UITouch的常用属性
1.4 UITouch的常用方法
2.UIEvent
3.UIResponder详解
4.手势UIGestureRecognizer
5.响应链
6.iOS触摸事件详解
6.1 事件传递
6.2 事件转发
6.3 事件响应
7.Touch Event、UIGestureRecognizer、响应链之间的关系
7.1 关联详解
7.2 拦截
总结

1. UITouch

1.1 UITouch的创建

当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象,一根手指对应一个UITouch对象。

1.2 UITouch的作用

UITouch保存着跟手指相关的信息,比如触摸的位置、时间、阶段等。
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。
当手指离开屏幕时,系统会销毁相应的UITouch对象。

1.3 UITouch的常用属性
/// 触摸产生时所处的窗口。由于窗口可能发生变化,当前所在的窗口不一定是最开始的窗口
@property(nonatomic,readonly,retain) UIWindow *window;

/// 触摸产生时所处的视图。由于视图可能发生变化,当前视图也不一定时最初的视图
@property(nonatomic,readonly,retain) UIView *view;

/// 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger  tapCount;

/// 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;

/// 当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase  phase;

//Touch 状态枚举
typedef NS_ENUM(NSInteger, UITouchPhase) {
    UITouchPhaseBegan,             // whenever a finger touches the surface.
    UITouchPhaseMoved,             // whenever a finger moves on the surface.
    UITouchPhaseStationary,        // whenever a finger is touching the surface but hasn't moved since the previous event.
    UITouchPhaseEnded,             // whenever a finger leaves the surface.
    UITouchPhaseCancelled,         // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
1.4 UITouch的常用方法
- (CGPoint)locationInView:(UIView *)view;
//返回值表示触摸在view上的位置
//这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
//调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置

- (CGPoint)previousLocationInView:(UIView *)view;
//记录了前一个触摸点的位置

2.UIEvent官方文档

1、触摸事件:第一个手指开始触摸屏幕到最后一个手指离开屏幕定义为一个触摸事件。

2、UIEvent实际包括了多个UITouch对象。有几个手指触碰,就会有几个UITouch对象。

@interface UIEvent : NSObject
@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype  subtype NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) NSTimeInterval  timestamp;
#if UIKIT_DEFINE_AS_PROPERTIES

@property(nonatomic, readonly, nullable) NSSet <UITouch *> *allTouches;
//省略部分代码
@end
typedef NS_ENUM(NSInteger, UIEventType) {
    UIEventTypeTouches, // 触摸事件
    UIEventTypeMotion, // 加速计事件
    UIEventTypeRemoteControl, // 远程事件
    UIEventTypePresses , // 物理按压事件
};

typedef NS_ENUM(NSInteger, UIEventSubtype) {
    // available in iPhone OS 3.0
    UIEventSubtypeNone                              = 0,
    
    // for UIEventTypeMotion, available in iPhone OS 3.0
    UIEventSubtypeMotionShake                       = 1,
    
    // for UIEventTypeRemoteControl, available in iOS 4.0
    UIEventSubtypeRemoteControlPlay                 = 100,
    UIEventSubtypeRemoteControlPause                = 101,
    UIEventSubtypeRemoteControlStop                 = 102,
    UIEventSubtypeRemoteControlTogglePlayPause      = 103,
    UIEventSubtypeRemoteControlNextTrack            = 104,
    UIEventSubtypeRemoteControlPreviousTrack        = 105,
    UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
    UIEventSubtypeRemoteControlEndSeekingBackward   = 107,
    UIEventSubtypeRemoteControlBeginSeekingForward  = 108,
    UIEventSubtypeRemoteControlEndSeekingForward    = 109,
};

UIEvent中包含若干UITouch,当某个UITouch对象的phase状态发生变化,系统会产生一条TouchMessage,继而传递和派发Touch message。
也就是说每次用户手指的移动和变化,UITouch都会形成状态改变,系统便会产生TouchMessage。一次触摸事件是由一组UITouch对象状态变化引起的一组Touch message的传递和转发。

3 UIResponder详解

3.1 UIResponder类可以接收并处理事件。

3.2 UIApplication、AppDelegate、UIViewController、UIView均继承UIResponder类。

3.3 UIResponder类的nextResponser。

Return value:UIView implements this method by returning the UIViewController object that manages it (if it has one) or its superview (if it doesn’t);UIViewController implements the method by returning its view’s superview; UIWindow returns the application object, and UIApplication returns nil.

1)UIView的nextResponder,如果存在管理的UIViewController对象,返回UIViewController对象,如果没有,返回父视图;

2)UIViewController的nextResponder是self.view;

3)UIWindow的nextResponder是application对象;

4)Application的nextResponder是nil;

@interface UIResponder : NSObject <UIResponderStandardEditActions>

//触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);

//物理按钮,遥控器上面的按钮在按压状态等状态下的回调
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);

//设备的陀螺仪和加速传感器使用
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);

// 省略部分代码
@end

4.手势UIGestureRecognizer

为没有继承UIControl的视图对象添加响应事件

4.1 UIGestureRecognizer类包含UIResponder类中的以下方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
4.2 手势状态
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    //未知状态
    UIGestureRecognizerStatePossible,   // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
    //首次识别状态,对于连续手势,例如长按,有这种状态
    UIGestureRecognizerStateBegan,      // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
    //再次识别,当手连续手势识别之后,再次受到touch事件
    UIGestureRecognizerStateChanged,    // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
    //识别完成,受到touchend 消息之后
    UIGestureRecognizerStateEnded,      // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
    //取消识别
    UIGestureRecognizerStateCancelled,  // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
    //识别失败
    UIGestureRecognizerStateFailed,     // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
    // Discrete Gestures – gesture recognizers that recognize a discrete event but do not report changes (for example, a tap) do not transition through the Began and Changed states and can not fail or be cancelled
    //识别状态,与识别结束一个意思
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
};
image.png

结合图我们来看手势的整个迁移过程,先明确几个信息

1)手势的状态迁移,前提是收到Touch message,才能做状态变化处理代码。
2)手势分为连续状态手势、不连续状态手势。连续手势有长按,慢滑等。不连续手势有单击,双击等等。
3)当用户没有点击屏幕,所有手势都处于Possiable初始状态。
当用户点击屏幕,手势会收到Touch Began Message, 手势的touchBegan方法会被调用,手势开始记录点击位置和时间,仍处于 Possiable状态。

如果用户按住不放,间隔超过一定时间,单击手势会变化为Failed状态,并在下个一runloop变为possiable。

如果时间大于长按手势设定时间,长按手势就会变化为Began状态,当用户移动手指,长按手势的touch move方法被调用,长按手势将自己状态设置为Changed状态,并且也会回调处理方法。最后手指离开,系统调用长按手势touchEnd方法,手势状态设置为 Recognized状态。

4.3 混合手势处理

1)当给UIView添加多个UIGestureRecognizer对象时,默认只有1个生效。如果想全部都生效,让协议中的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法返回YES。

2)同时添加单击和双击,也均允许生效。问题来了,那双击屏幕时,默认触发1次单击事件和1次双击事件。但这不是想要的效果,如何实现双击时,只触发双击手势呢,单击时只触发单击手势呢?解决方案是让协议中的gestureRecognizer:shouldRequireFailureOfGestureRecognizer:方法、

gestureRecognizer:shouldBeRequiredToFailByGestureRecognizer:方法都返回YES。

4.4 UIGestureRecognizerDelegate协议
@protocol UIGestureRecognizerDelegate <NSObject>
@optional

// 手势状态是否允许更改,默认为YES。
// 如果实现中返回NO,那么手势最后都为失败状态。
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

// 允许多个手势生效,默认为NO。
// 如果实现中返回YES,同时添加单击和双击手势,双击屏幕时,同时产生1次单击事件和1次双击事件
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

// 以下2个方法,为手势之间添加依赖,默认NO。
// 比如单击和双击,如果双击手势识别失败,转换为识别单击手势
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

// 手势是否关注UITouch、UIPress对象状态变化,和gestureRecognizerShouldBegin:效果类似,默认为YES。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

@end
4.5 苹果封装以下几个手势

(1)UITapGestureRecognizer

单个或多个塞子。指定数量的手指必须要承认的姿态,挖掘查看指定的次数。

(2)UIPinchGestureRecognizer

看起来捏的手势,涉及两个接触。当用户将两个手指,向对方的传统意义是缩小;当用户将两个手指从彼此远离,传统意义变焦。

(3)UIRotationGestureRecognizer

看起来轮换涉及两个触摸手势。当用户移动手指对面对方的圆周运动,基本的观点应该在相应的方向和速度旋转。

(4)UISwipeGestureRecognizer

看起来刷卡在一个或多个方向的手势。抨击是一个独立的姿态,因此,相关的操作的消息发送每个手势只有一次。

(5)UIPanGestureRecognizer

看起来平移(拖动)的手势。用户必须按查看上一个或更多的手指,而他们平移。实施这个手势识别动作方法的客户端可以要求它目前的翻译和手势的速度。

(6)UILongPressGestureRecognizer

看起来长按手势。用户必须按下一个或更多的手指行动讯息传送至少指定期限。此外,手指可能要承认的姿态移动唯一指定的距离;如果他们超越这个限制的姿态失败。

5. 响应链

响应链由UIResponder对象为node,形成的一个链表状结构,通过UIResonder的nextResonder链接。
下图中节点关系是箭头方向朝上,也就是subView指向superView。


image.png
image.png
image.png
image.png

6.iOS触摸事件详解

事件传递是在响应链中查找hitTestView的过程,从父View到子View。事件转发是响应消息中触发的,从子View通过nextResponder在响应链中回溯。

6.1 事件传递

1)找到设备中的Application。

触摸屏幕时,由iOS系统的硬件进程获取,简单封装事件后暂存在系统中,利用端口实现与Application进程完成通信,将事件传递给Application进程。

当应用程序启动时,主线程的RunLoop会注册一个基于端口的source,当接收到相关事件时,主线程会被唤醒执行触摸事件。

2)通过响应链找到最终处理事件的hitTestView。

当Application接收到新的事件时,开始寻找响应链中的hitTestView。

将所有的显示在屏幕上的 "UIWindow对象",按照层级结构从上到下排列成一个数组。从第一个UIWindow对象开始,先判断UIWindow是否不隐藏且可见度大于0.01且可交互,再判断点击位置在不在这个UIWindow内。

如果不在 ,返回nil, 就换下一个UIWindow;

如果在的话,并且UIWindow没有subView就返回自己,但如果UIWindow有subViews,就递归遍历整个subViews,直到找到hitTestView。

如果没有找到到就不做传递。

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
  //判断该视图是否满足响应事件的条件
    if (!self.hidden && self.alpha > 0.01 && self.isUserInteractionEnabled) {
        //判断点击位置是否在自己区域内部
        if ([self pointInside: point withEvent:event]) {
            UIView *attachedView;
            // 遍历子视图
            for (int i = self.subviews.count - 1; i >= 0; i--) {
                UIView *view  = self.subviews[i];
                // 对子view递归调用本方法
                attachedView =  [view hitTest:point withEvent:event];
                if (attachedView)
                    break;
            }
            if (attachedView)  {
                return attachedView;
            } else {
                return self;
            }
        }
    }
    return nil;
}
6.2 事件转发
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    //do someThiing
  [self.nextResponser touchesBegan: touches withEvent:event];
}
6.3 事件响应

当锁定hitTestView后,当触摸状态发生变化,会不停的收到UITouch Message消息,调用hitTestView从UIResponder类继承的方法。

// 点击刚开始,回调这个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 点击之后移动,回调这个方法
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 手指移开、点击结束,回调这个方法
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
// 事件被手势识别,回调这个方法
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

7.Touch Event、UIGestureRecognizer、响应链之间的关系

image.png
7.1 关联详解

第一步:系统会将所有的Touch message优先发送给关联在响应链上的全部手势。手势根据Touch序列消息和手势基本规则更改自己的状态(有的可能失败,有的可能识别等等)。如果某个手势对Touch message成功拦截(被拦截时,系统不会将Touch message 发送给响应链顶部响应者),顶部视图控件调用touchesCancelled:withEvent方法,否则系统会进入第二步。

第二步:系统将Touch message发送给响应链顶部的视图控件,顶部视图控件这个时候就会调用Touch相关的四个方法中的某一个。

7.2 拦截

举例说明:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleClick)];
    gesture.numberOfTapsRequired = 2;
    self.view.userInteractionEnabled = YES;

    [self.view addGestureRecognizer:gesture];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan");
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesEnded");
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesMoved");
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesCancelled");
}

- (void)doubleClick {
    NSLog(@"双击拉");
}

结果分析:
1)如果单击屏幕(拦截失败),打印
TestForMoreGesture[26097:18114462] touchesBegan
TestForMoreGesture[26097:18114462] touchesEnded
2)如果双击屏幕(拦截成功),打印
TestForMoreGesture[26097:18114462] touchesBegan
TestForMoreGesture[26097:18114462] 双击拉
TestForMoreGesture[26097:18114462] touchesCancelled

手势是否拦截该Touch Message,主要由UIGestureRecognizer类的三个属性控制。

// 默认为YES,表明当手势成功识别事件后,系统会将Touch cancel消息发送给hitTestView ,并调用hitTestView的TouchCancel。设置为NO,不会再收到TouchCancel
@property(nonatomic) BOOL cancelsTouchesInView;
// 默认为YES, 表明无论什么情况下,不会拦截Touch began消息。如果设置为NO,只要有一个手势不识别失败,都不会发送Touch began到响应链的第一响应者。
@property(nonatomic) BOOL delaysTouchesBegan; 
// 默认为NO, 和delaysTouchesBegan类似,不过它是用来控制TouchEnd message的拦截
@property(nonatomic) BOOL delaysTouchesEnded; 

总结

iOS整个事件处理的过程就是这样,系统为完成整个交互做了很多东西,核心点如下:

1、事件分发过程分为:1.事件消息传递;2.事件消息分发。

2、响应网是事件响应的基础,响应链是事件响应的具体路径。

3、事件消息分发优先发送给手势集合,手势内部会做冲突处理,过滤消息。不被过滤的消息会传递给响应链对象。

参考文章:
iOS触摸事件详解
深入浅出iOS事件机制