iOS开发基础29-事件处理深度解析:事件类型、响应者链条、Hit-Test 与手势识别

iOS 事件处理深度解析:事件类型、响应者链条、Hit-Test 与手势识别

本文系统梳理 iOS 事件处理机制:三大事件类型、UIResponder 响应者对象、触摸事件与 UITouch/UIEvent、事件产生与 Hit-Test 传递机制、响应者链条、手势识别器及其与触摸事件的关系。


一、iOS 中的事件类型

iOS 事件分为三大类型:

事件类型 说明 触发场景
触摸事件(UIEventTypeTouches) 用户触摸屏幕产生 点击、滑动、捏合、旋转等
运动事件(UIEventTypeMotion) 设备运动产生 摇一摇(UIEventSubtypeMotionShake)
远程控制事件(UIEventTypeRemoteControl) 外部控制产生 耳机线控、控制中心播放/暂停/切歌

运动事件中,UIResponder 的 motionBegan/motionEnded 主要用于摇一摇。更复杂的加速度计、陀螺仪、磁力计数据通过 Core Motion 框架(CMMotionManager)获取,不经过 UIResponder 事件链。


二、响应者对象(Responder Object)

只有继承自 UIResponder 的对象才能接收和处理事件,称为响应者对象。

UIResponder
├── UIView
│   ├── UIWindow
│   ├── UIControl(UIButton、UITextField 等)
│   ├── UILabel(默认 userInteractionEnabled = NO,不响应触摸)
│   └── UIImageView(默认 userInteractionEnabled = NO,不响应触摸)
├── UIViewController
└── UIApplication

UILabel 和 UIImageView 默认 userInteractionEnabled = NO,无法响应触摸事件。如需响应,需手动设置 userInteractionEnabled = YES。

第一响应者

第一响应者(First Responder)是当前接收事件的响应者对象,如正在编辑的 UITextField。

// 成为第一响应者(如弹出键盘)
[textField becomeFirstResponder];

// 辞去第一响应者(如收起键盘)
[textField resignFirstResponder];

// 判断是否是第一响应者
BOOL isFirst = [textField isFirstResponder];

三、UIResponder 事件处理方法

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;

运动事件(摇一摇)

- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

远程控制事件

- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

摇一摇事件需要视图控制器或视图成为第一响应者(becomeFirstResponder),且 canBecomeFirstResponder 返回 YES。


四、UIView 的触摸事件处理

UIView 继承自 UIResponder,可重写触摸方法:

@interface CustomView : UIView
@end

@implementation CustomView

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

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

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

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"触摸取消(如来电、手势识别成功)");
}

@end

重写触摸方法时,建议调用 [super touchesBegan:...](如果需要事件继续沿响应者链条传递)。不调用 super 则事件被当前视图"吃掉",不再向上传递。


五、UITouch 对象

每根手指触摸屏幕时创建一个 UITouch 对象,一根手指对应一个 UITouch,在整个触摸过程中保持同一个对象。

常用属性

属性 类型 说明
window UIWindow 触摸产生时所处的窗口
view UIView 触摸产生时所处的视图
tapCount NSUInteger 连续点击次数(区分单击/双击)
timestamp NSTimeInterval 触摸事件的时间戳
phase UITouchPhase 触摸阶段(Began/Moved/Stationary/Ended/Cancelled)
force CGFloat 按压力度(3D Touch,iOS 9+)
maximumPossibleForce CGFloat 最大可能按压力度

常用方法

// 获取触摸点在指定视图中的位置(坐标相对于 view 的左上角)
CGPoint location = [touch locationInView:self.view];

// 获取上一个触摸点的位置(用于计算移动距离)
CGPoint previousLocation = [touch previousLocationInView:self.view];

UITouchPhase 枚举

typedef NS_ENUM(NSInteger, UITouchPhase) {
    UITouchPhaseBegan,         // 手指刚接触屏幕
    UITouchPhaseMoved,         // 手指在屏幕上移动
    UITouchPhaseStationary,    // 手指停留在屏幕上(未移动)
    UITouchPhaseEnded,         // 手指离开屏幕
    UITouchPhaseCancelled,     // 触摸被取消(来电、系统中断、手势识别成功)
};

六、UIEvent 对象

每产生一个事件,系统创建一个 UIEvent 对象,记录事件的类型、子类型、时间戳和所有触摸对象。

常用属性

属性 说明
type 事件类型(UIEventTypeTouches/Motion/RemoteControl/Presses)
subtype 事件子类型(如 UIEventSubtypeMotionShake)
timestamp 事件时间戳
allTouches 事件包含的所有 UITouch 对象

常用方法

// 获取指定视图上的所有触摸对象
NSSet<UITouch *> *touches = [event touchesForView:self.view];

// 获取指定窗口上的所有触摸对象
NSSet<UITouch *> *windowTouches = [event touchesForWindow:self.view.window];

七、事件的产生与传递(Hit-Test)

1. 事件产生与分发流程

用户触摸屏幕
    ↓
系统(IOKit)识别触摸,封装为 UIEvent
    ↓
UIApplication 从事件队列取出事件
    ↓
分发给 keyWindow(主窗口)
    ↓
keyWindow 调用 hitTest:withEvent: 查找最合适的视图
    ↓
找到的视图成为事件的第一响应者,调用其 touches 方法

2. hitTest:withEvent: 底层实现

hitTest:withEvent: 是 UIView 的方法,系统在事件分发时调用,返回最合适处理事件的视图:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1. 判断当前视图能否接收事件
    if (self.userInteractionEnabled == NO ||   // 禁止交互
        self.hidden == YES ||                    // 隐藏
        self.alpha <= 0.01) {                   // 几乎透明
        return nil;
    }
    
    // 2. 判断触摸点是否在当前视图范围内
    if ([self pointInside:point withEvent:event] == NO) {
        return nil;
    }
    
    // 3. 从后往前遍历子视图(后添加的子视图在层级上方,优先响应)
    for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
        // 将触摸点坐标转换到子视图坐标系
        CGPoint convertedPoint = [subview convertPoint:point fromView:self];
        // 递归调用子视图的 hitTest
        UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
        if (hitView) {
            return hitView; // 子视图能处理,返回子视图
        }
    }
    
    // 4. 没有子视图能处理,自己处理
    return self;
}

3. pointInside:withEvent:

pointInside:withEvent: 判断触摸点是否在视图范围内,可重写来扩大点击区域:

// 扩大按钮点击区域(点击范围比可视范围大 20pt)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect bounds = self.bounds;
    CGFloat widthDelta = MAX(44.0 - bounds.size.width, 0);
    CGFloat heightDelta = MAX(44.0 - bounds.size.height, 0);
    bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
    return CGRectContainsPoint(bounds, point);
}

Apple 推荐可点击控件的最小点击区域为 44x44pt,小于此尺寸的按钮可通过重写 pointInside:withEvent: 扩大点击区域。


八、响应者链条

响应者链条(Responder Chain)是由多个响应者对象按顺序连接起来的链条,事件可沿链条向上传递,让多个对象有机会处理同一个事件。

1. 完整响应者链条

当前视图(hitTest 找到的视图)
    ↓(如果不处理或调用 super)
父视图(superview)
    ↓
...(逐级向上)
    ↓
UIWindow
    ↓
UIApplication
    ↓
UIApplication 的 delegate(AppDelegate,如果是 UIResponder 子类)

2. 上一个响应者(nextResponder)的判断

当前对象 上一个响应者
普通 UIView 父视图(superview)
UIViewController 的 view 该 UIViewController
UIViewController 父视图控制器 或 UIWindow(如果是根控制器)
UIWindow UIApplication
UIApplication AppDelegate(如果是 UIResponder 子类)

3. 事件沿响应者链条传递的完整过程

1. 事件由上往下传递(hitTest),找到最合适的视图
       ↓
2. 调用最合适视图的 touchesBegan/Moved/Ended
       ↓
3. 如果该视图调用了 [super touches...],事件传递给上一个响应者
       ↓
4. 上一个响应者的 touches 方法被调用
       ↓
5. 重复 3-4,直到某个响应者处理了事件(不调用 super)或到达链条末端
       ↓
6. 到达 UIApplication/AppDelegate 仍未处理,事件被丢弃

默认情况下,UIView 的 touchesBegan 等方法不调用 super(事件不向上传递)。UIControl(如 UIButton)通过 target-action 机制处理事件,也不沿响应者链条传递。


九、手势识别(UIGestureRecognizer)

iOS 3.2 引入手势识别器,将触摸事件的识别逻辑封装,大大简化了复杂手势的处理。

1. 常见手势识别器

手势识别器 说明
UITapGestureRecognizer 点按(单击/双击/多击)
UIPinchGestureRecognizer 捏合(缩放)
UIPanGestureRecognizer 拖拽(平移)
UISwipeGestureRecognizer 轻扫(快速滑动)
UIRotationGestureRecognizer 旋转
UILongPressGestureRecognizer 长按

2. 使用步骤

// 1. 创建手势识别器
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];

// 2. 设置属性
tap.numberOfTapsRequired = 2;        // 双击
tap.numberOfTouchesRequired = 1;     // 一根手指

// 3. 添加到视图(视图的 userInteractionEnabled 必须为 YES)
[self.view addGestureRecognizer:tap];

// 4. 实现手势处理方法
- (void)handleTap:(UITapGestureRecognizer *)tap {
    NSLog(@"双击手势识别成功");
}

一个手势识别器只能添加到一个视图,但一个视图可以添加多个手势识别器。

3. 各种手势示例

长按

UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
longPress.minimumPressDuration = 1.0; // 最短按 1 秒
[self.view addGestureRecognizer:longPress];

- (void)handleLongPress:(UILongPressGestureRecognizer *)longPress {
    if (longPress.state == UIGestureRecognizerStateBegan) {
        NSLog(@"长按开始");
    }
}

轻扫

// 一个 UISwipeGestureRecognizer 实例只能识别一个方向,需要多个方向需创建多个实例
UISwipeGestureRecognizer *swipeRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipeRight.direction = UISwipeGestureRecognizerDirectionRight;
[self.view addGestureRecognizer:swipeRight];

UISwipeGestureRecognizer *swipeLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipe:)];
swipeLeft.direction = UISwipeGestureRecognizerDirectionLeft;
[self.view addGestureRecognizer:swipeLeft];

- (void)handleSwipe:(UISwipeGestureRecognizer *)swipe {
    if (swipe.direction == UISwipeGestureRecognizerDirectionRight) {
        NSLog(@"向右轻扫");
    } else if (swipe.direction == UISwipeGestureRecognizerDirectionLeft) {
        NSLog(@"向左轻扫");
    }
}

旋转

UIRotationGestureRecognizer *rotation = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(handleRotation:)];
[self.imageView addGestureRecognizer:rotation];

- (void)handleRotation:(UIRotationGestureRecognizer *)rotation {
    // rotation.rotation 是自手势开始以来的累计旋转角度(弧度)
    self.imageView.transform = CGAffineTransformRotate(self.imageView.transform, rotation.rotation);
    rotation.rotation = 0; // 重置,避免累计
}

捏合(缩放)

UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
[self.imageView addGestureRecognizer:pinch];

- (void)handlePinch:(UIPinchGestureRecognizer *)pinch {
    // pinch.scale 是自手势开始以来的累计缩放比例
    self.imageView.transform = CGAffineTransformScale(self.imageView.transform, pinch.scale, pinch.scale);
    pinch.scale = 1.0; // 重置,避免累计
}

拖拽(平移)

UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
[self.dragView addGestureRecognizer:pan];

- (void)handlePan:(UIPanGestureRecognizer *)pan {
    CGPoint translation = [pan translationInView:self.view];
    self.dragView.center = CGPointMake(self.dragView.center.x + translation.x,
                                        self.dragView.center.y + translation.y);
    [pan setTranslation:CGPointZero inView:self.view]; // 重置
}

4. 手势识别状态

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,   // 可能识别(等待触摸事件)
    UIGestureRecognizerStateBegan,      // 手势开始(连续手势)
    UIGestureRecognizerStateChanged,    // 手势变化(连续手势)
    UIGestureRecognizerStateEnded,      // 手势结束(连续手势)/ 识别成功(离散手势)
    UIGestureRecognizerStateCancelled,  // 手势取消
    UIGestureRecognizerStateFailed,     // 手势识别失败
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};
手势类型 状态序列
离散手势(Tap/Swipe) Possible → Ended(或 Failed)
连续手势(Pan/Pinch/Rotation/LongPress) Possible → Began → Changed → Ended(或 Cancelled/Failed)

5. 手势识别与触摸事件的关系

手势识别器优先于视图的触摸事件处理:

触摸事件产生
    ↓
系统先将事件发送给视图的手势识别器
    ↓
手势识别器识别中 → 视图收到 touchesBegan(取决于 delaysTouchesBegan)
    ↓
手势识别成功 → 向视图发送 touchesCancelled(取消视图的触摸处理)
    ↓
手势识别失败 → 视图收到 touchesEnded(继续处理触摸事件)

控制手势与触摸事件关系的属性

属性 默认值 说明
cancelsTouchesInView YES 手势识别成功后,向视图发送 touchesCancelled,取消视图的触摸处理
delaysTouchesBegan NO 是否延迟 touchesBegan 到手势识别失败后(YES 时视图不会立即收到 touchesBegan)
delaysTouchesEnded YES 是否延迟 touchesEnded 到手势识别失败后(YES 时手势识别失败后视图才收到 touchesEnded)

6. 多手势同时识别

默认情况下,一个视图上的多个手势识别器互斥(一个识别成功后其他失败)。通过 UIGestureRecognizerDelegate 可支持同时识别:

@interface ViewController () <UIGestureRecognizerDelegate>
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    pinch.delegate = self;
    rotation.delegate = self;
}

// 允许多个手势同时识别(如同时缩放和旋转)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

@end

7. 手势依赖关系

通过 requireGestureRecognizerToFail: 设置手势的依赖关系(一个手势必须等另一个手势失败后才能识别):

// 双击手势必须等单击手势失败后才能识别(避免单击和双击冲突)
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] init];
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] init];
doubleTap.numberOfTapsRequired = 2;

// 单击手势需要双击手势失败后才能识别
[singleTap requireGestureRecognizerToFail:doubleTap];

十、Swift 版本对照

触摸事件

class CustomView: UIView {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        print("触摸开始")
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let touch = touches.first {
            let location = touch.location(in: self)
            let previous = touch.previousLocation(in: self)
            print("移动: \(location), 上次: \(previous)")
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("触摸结束")
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        print("触摸取消")
    }
}

Hit-Test

class CustomView: UIView {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard isUserInteractionEnabled, !isHidden, alpha > 0.01 else { return nil }
        guard point(inside: point, with: event) else { return nil }
        
        for subview in subviews.reversed() {
            let convertedPoint = subview.convert(point, from: self)
            if let hitView = subview.hitTest(convertedPoint, with: event) {
                return hitView
            }
        }
        return self
    }
    
    // 扩大点击区域
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let inset = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
        return bounds.inset(by: inset).contains(point)
    }
}

手势识别

class ViewController: UIViewController, UIGestureRecognizerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 点按
        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        tap.numberOfTapsRequired = 2
        view.addGestureRecognizer(tap)
        
        // 拖拽
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
        view.addGestureRecognizer(pan)
        
        // 捏合
        let pinch = UIPinchGestureRecognizer(target: self, action: #selector(handlePinch(_:)))
        pinch.delegate = self
        view.addGestureRecognizer(pinch)
        
        // 旋转
        let rotation = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:)))
        rotation.delegate = self
        view.addGestureRecognizer(rotation)
    }
    
    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
        print("双击")
    }
    
    @objc func handlePan(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        // 移动视图...
        gesture.setTranslation(.zero, in: view)
    }
    
    @objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
        view.transform = view.transform.scaledBy(x: gesture.scale, y: gesture.scale)
        gesture.scale = 1.0
    }
    
    @objc func handleRotation(_ gesture: UIRotationGestureRecognizer) {
        view.transform = view.transform.rotated(by: gesture.rotation)
        gesture.rotation = 0
    }
    
    // 多手势同时识别
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

十一、总结

  • 事件类型:触摸事件、运动事件(摇一摇)、远程控制事件。复杂传感器数据通过 Core Motion 获取,不经过 UIResponder。
  • 响应者对象:继承 UIResponder 的对象才能处理事件;UILabel/UIImageView 默认 userInteractionEnabled=NO 不响应触摸。
  • UITouch:每根手指对应一个对象,locationInView:/previousLocationInView: 获取位置,phase 表示触摸阶段,force 表示按压力度。
  • UIEvent:记录事件类型、子类型、时间戳和所有触摸对象。
  • Hit-Test:事件从 UIApplication → keyWindow,通过 hitTest:withEvent: 从上往下递归查找最合适的视图;判断条件为 userInteractionEnabled/hidden/alpha 和 pointInside;从后往前遍历子视图。
  • 响应者链条:view → superview → ... → UIWindow → UIApplication → AppDelegate;调用 [super touches...] 可将事件向上传递。
  • 手势识别:6 种手势识别器,优先于视图触摸事件处理;识别成功后默认向视图发送 touchesCancelled;通过 delegate 支持多手势同时识别,通过 requireGestureRecognizerToFail 设置依赖关系。
  • 手势与触摸关系:cancelsTouchesInView/delaysTouchesBegan/delaysTouchesEnded 控制手势与视图触摸事件的交互。

posted @ 2015-08-02 21:02  Mr.陳  阅读(3205)  评论(2)    收藏  举报