iOS——事件、响应链和传递链

UIControl、UIEvent、UITouch、UIPress、UIResponder

在讨论事件、响应链和传递链之前,我们先来认识一下相关的几个类。

UIControl

该类下的核心方法大家应该都见过:

- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;

所以所有能调用这个方法的控件,比如UIButton、UITextField等等,都是继承自这个类。

UIControl的主要功能:为一些交互类控件添加action。它控制一个控件在某个状态下(比如按下、抬起等)要发生什么事件。

UIEvent

是由硬件捕获到的一个表示用户操作设备的对象,事件分为三类:包括触摸事件(Touch Events对应就是UITouch)、运动事件(Motion Events)、远程控制事件(Remote Control Events)。在这里插入图片描述

UITouch

紧接着UIEvent,UITouch这个类就详细地表明的手指触摸屏幕的这个动作的具体参数,比如触摸类型(直接一根手指、还是用触摸笔)、触摸状态(一根手指开始接触屏幕、在屏幕上移动等等)、触摸力度、触摸事件、几个手指触摸等等。

总结一下,这个类是用来定义实际触摸动作的参数。

@interface UITouch : NSObject

@property(nonatomic,readonly) NSTimeInterval      timestamp; // 触摸发起的时间
@property(nonatomic,readonly) UITouchPhase        phase; // 触摸的各个阶段状态
@property(nonatomic,readonly) NSUInteger          tapCount; // 快速双击
@property(nonatomic,readonly) UITouchType         type API_AVAILABLE(ios(9.0)); //手指 or pencil

@property(nullable,nonatomic,readonly,strong) UIWindow                        *window;
@property(nullable,nonatomic,readonly,strong) UIView                          *view;
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers 
...
@end

// 触摸的各个阶段状态, 
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)
    UITouchPhaseRegionEntered   API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // whenever a touch is entering the region of a user interface
    UITouchPhaseRegionMoved     API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // when a touch is inside the region of a user interface, but hasn’t yet made contact or left the region
    UITouchPhaseRegionExited    API_AVAILABLE(ios(13.4), tvos(13.4)) API_UNAVAILABLE(watchos),  // when a touch is exiting the region of a user interface
};

UIPress

UIPress跟UITouch类似:UITouch表示触摸的相关信息,UIPress则表示按压的相关信息。

UIResponder

UIResponder是响应对象的基类,定义了处理各种事件的接口,专门用来响应用户的操作和处理各种事件。常见的子类有:UIView,UIViewController,UIApplication和UIApplicationDelegate。

UIResponder提供了用户点击、按压检测以及手势检测的回调方法,分别对应用户开始、移动、结束以及取消,其中只有在程序强制退出或者来电时,取消事件才会调用。

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 //当手指触摸过程被其他行为中断等异常情况触发
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0)) //用户用力按下屏幕开始触发
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0)) //用户用力按下屏幕发生压力变化触发
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0)) //用户按压结束触发
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event //用户按压行为被打断触发,IOS9以上支持
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event //陀螺仪或加速设备开始发生改变触发
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event //陀螺仪或加速设备发生改变过程触发
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event //陀螺仪或加速设备改变过程被其他行为中断异常触发

UIViewController利用UIResponder实例

// UIViewController.m
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"屏幕被手指按下了");
    // 获取手指列表
    NSArray<UITouch*>* toucheList = [touches allObjects];
    for (int i = 0; i < toucheList.count; i ++) {
        UITouch* touch = toucheList[i];
        NSLog(@"获取手指15s内同一位置点击次数 -> 手指 %d 15s内点击了 %zd 次",  i, touch.tapCount);
    }
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"手指移动啦");
    // 获取手指列表
    UITouch* touch = [touches allObjects][0];
    // 获取手指相对于指定视图的x,y轴
    CGPoint position = [touch locationInView:self.view];
    NSLog(@"手指坐标为x = %f,y = %f", position.x, position.y);
}

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

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"触摸过程被其他行为异常中断");
}

事件

iOS中的事件分成好几种:

  • 触摸事件,手指触摸屏幕时产生
  • 加速事件,手机的陀螺仪以及加速度计产生的
  • 远程控制事件,使用其他远程控制设备控制,例如蓝牙设备

下面,我们讨论的都是触摸事件。

事件流程

整个事件传递和处理流程,简单概括为:

事件——事件传递到指定界面——找到可响应的界面——响应

  • 当有用户触摸屏幕的时候产生事件,系统硬件进程获取到这个事件,并处理封装保存在系统中,由于系统硬件进程和app进程是两个不同的进程,所以使用进程间的端口通信。
  • 系统会将这个事件加入到UIApplication的事件管理队列中,事件从队列中出队后通常会发送给app的keywindow处理。
  • keywindow会找到一个最适合的视图去处理事件。也就是从super控件到子控件中。

简单总结事件流程:UIApplication->window->寻找处理事件最合适的view

下面我们详细讨论这个过程。

事件链

在这里插入图片描述
以上图为例:

点击屏幕时,首先UIApplication对象先收到该点击事件,再依次传递给它上面的所有子view,直到传递到最上层。

我们有传递链:UIApplication——>UIWindow——>RootViewController——>View——>Button,即绿色箭头。

反之,也有响应链Button——>View——>RootViewController——>UIWindow——>UIApplication,即红色箭头。

简单总结,事件链包含传递链和响应链,一般事件先通过传递链传递下去。如果上层不能响应,那么一层一层通过响应链找到能响应的UIResponse。

传递链

传递链由系统向最上层view传递:Application -> window -> root view -> ... -> first view

具体过程

  1. 当触摸事件发生时,压力传为电信号,iOS系统将产生UIEvent对象,记录事件产生的时间和类型。
  2. 当检测到一个系统事件、例如屏幕上的点击,UIKit内部创建一个UIEvent实例并且记录事件产生的时间和类型,然后系统将事件加入到一个由UIApplication管理的事件队列中。
  3. UIApplication会从消息队列中取出该事件传递给UIWindow对象,在UIWindow中调用方法hitTest:withEvent:返回最终相应的view。
  4. hitTest:withEvent:方法中调用pointInside:withEvent:来判断当前点击的点是否在UIWindow内部,如若返回yes,则倒序遍历其子视图找到最终响应的子view。
  5. 如果最终返回一个view,那么即为最终响应view并结束事件传递,如果无值返回则将UIWindow作为响应者。

过程中提到的两个核心方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

hitTest:withEvent:

该方法的处理流程:

  1. 首先调用当前视图的pointInside:withEvent: 方法判断触摸点是否在当前视图内。
  2. 若返回NO,则hitTest:withEvent: 返回 nil。
  3. 若返回YES,则向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶视图一直到最低层视图,即从 subviews 数组的末尾向前遍历,直到有子视图返回非空对象,或者全部子视图遍历完毕。
  4. 若第一次有子视图返回非空对象,则hitTest:withEvent:返回此对象,处理结束。
  5. 若所有子视图都返回空,则hitTest:withEvent:返回自身。

具体处理案例:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *touchView = self;
    if ([self pointInside:point withEvent:event] &&
       (!self.hidden) &&
       self.userInteractionEnabled &&
       (self.alpha >= 0.01f)) {
        for (UIView *subView in self.subviews) {
            [subview convertPoint:point fromView:self];
            UIView *subTouchView = [subView hitTest:subPoint withEvent:event];
            if (subTouchView) {
                touchView = subTouchView;
                break;
            }
        }
    } else {
        touchView = nil;
    }
    return touchView;
}

该方法不接受事件处理的情况

  1. hidden = YES,隐藏的视图
  2. userInteractionEnabled = NO,禁止用户操作的视图
  3. alpha <0.01, 透明视图

注意

  1. 如果最终hitTest 没有找到第一响应者,或第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯。若最终 UIWindow UIApplication都不能处理该事件,则会被丢弃。
  2. 如果一个子视图的区域超过父视图的 bound 区域(父视图的 clipsToBounds 属性为 NO,这样超过父视图 bound 区域的子视图内容也会显示),那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别, 因为父视图的 pointInside:withEvent: 方法会返回 NO, 这样就不会继续向下遍历子视图了。当然,也可以重写 pointInside:withEvent: 方法来处理这种情况。
  3. 我们可以重写 hitTest:withEvent: 来达到某些特定的目的。

响应链

在苹果的官方文档中,有使用响应者和响应者链来处理事件在这里插入图片描述

响应链由最基础的view向系统传递:
first view -> super view -> ... -> view controller -> window -> Application -> AppDelegate

响应链是 UIKit 生成的 UIResponder 对象组成的链表,它同时还是 iOS 里一切相关事件(例如触摸和动效)的基础。

当iOS捕获到某个事件时,就会将此事件传递给某个看上去最适合处理该事件的对象,比如触摸事件传递给手指刚刚触摸位置的那个视图(view),如果这个对象无法处理该事件,iOS系统就继续将该事件传递给更深层的对象,直到找到能够对该事件作出响应处理的对象为止。这一连串的对象序列被称作为“响应链”(responder chain),iOS系统就是沿着此响应链,由最外层逐步向内层对象传递该事件,亦即将 “处理该事件的责任” 进行传递。iOS的这种机制,使得事件处理具有协调性和动态性。

具体流程:

  1. 初始视图先进行尝试处理,自身处理不了的情况下,如果view的控制器存在,就传递给控制器处理;如果控制器不存在,则传递给它的父视图
  2. 在视图层次结构的最顶层,如果也不能处理收到的事件,则将事件传递给UIWindow对象进行处理
  3. 如果UIWindow对象也不处理,则将事件传递给UIApplication对象
  4. 如果UIApplication也不能处理该事件,则将该事件丢弃
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值