iOS开发-事件分发机制(hitTest与响应链)

一、概述:

ios事件分发机制即寻找当前 交互事件(UIEvent) 的最佳响应的View并回调该View的响应方法。

流程总体可抽象为画“V”字型,起点为UIApplication,底部顶点为最佳响应View,终点为消费事件的View

图解:
V字通常只有左侧(探查过程)+ 顶点(最佳响应View并回调touches等响应方法)而不会画完
有右侧通常是最佳响应View没有处理或进一步调用父View的touches响应方法
若均未消费则回到UIApplication才画完完整的“V”字


                  \         ·        ———————— UIApplication
                   \       ·
                    \     ·
                     \   ·
                      \ ·
                       ·             -------- 最佳响应View,回调touches方法               
  • 事件传递(“\”过程):hitTest:withEvent:方法(内含pointInside:withEvent:方法定位点击处View)寻找最佳响应的View(即“V”的顶点)
  • 事件响应(“/”过程):响应者链,touches系列方法响应

二、定义

https://www.cnblogs.com/wujy/p/5820825.html

1、事件分发的对象:UIEvent
  • UIEvent源码:
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @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;



//所有的触摸 
- (nullable NSSet <UITouch *> *)allTouches;
//获得UIWindow的触摸
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
//获得UIView的触摸  
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
//获得事件中特定手势的触摸
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture NS_AVAILABLE_IOS(3_2);


//会将丢失的触摸放到一个新的 UITouch 数组中
- (nullable NSArray <UITouch *> *)coalescedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);
//辅助UITouch的触摸,预测发生了一系列主要的触摸事件(可能不完全匹配的触摸的真正的行为)
- (nullable NSArray <UITouch *> *)predictedTouchesForTouch:(UITouch *)touch NS_AVAILABLE_IOS(9_0);

@end
  • UIEventType及UIEventSubType源码:
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) {
    // 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,
};

2、(触摸)事件序列:UITouches

UITouches是触摸事件(UIEventType = UIEventTypeTouches)的事件序列

  • 通常事件周期为:down * 1 -> move * n -> up * 1
//触摸事件周期
typedef NS_ENUM(NSInteger, UITouchPhase) {
    UITouchPhaseBegan,           //开始触摸 (down) 
    UITouchPhaseMoved,           //移动  (move)  
    UITouchPhaseStationary,      //停留 
    UITouchPhaseEnded,            //触摸结束 (up)
    UITouchPhaseCancelled,       //触摸中断
};

//检测是否支持3DTouch
typedef NS_ENUM(NSInteger, UIForceTouchCapability) {
    UIForceTouchCapabilityUnknown = 0,  //3D Touch检测失败
    UIForceTouchCapabilityUnavailable = 1,  //3D Touch不可用
    UIForceTouchCapabilityAvailable = 2  //3D Touch可用
};

NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject

//触摸产生或变化的时间戳 只读
@property(nonatomic,readonly) NSTimeInterval      timestamp;
//触摸周期内的各个状态
@property(nonatomic,readonly) UITouchPhase        phase;
//短时间内点击的次数 只读
@property(nonatomic,readonly) NSUInteger          tapCount;   

//获取手指与屏幕的接触半径 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
//获取手指与屏幕的接触半径的误差 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);

//触摸时所在的窗口 只读
@property(nullable,nonatomic,readonly,strong) UIWindow                        *window;
//触摸时所在视图
@property(nullable,nonatomic,readonly,strong) UIView                          *view;
//获取触摸手势
@property(nullable,nonatomic,readonly,copy)   NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

//取得在指定视图的位置
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0,0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(nullable UIView *)view;
//该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;

//获取触摸压力值,一般的压力感应值为1.0 IOS9 只读
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);

//获取最大触摸压力值
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);

@end

三、过程

1、事件传递(“\”过程)
  • 目的:寻找最佳响应View
  • 关键方法:hitTest:withEvent:(寻找最佳响应View) 和 pointInside:withEvent:(判断触摸点是否在该View内部)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 1、判断是否可以接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    
    // 2、判断点是否在当前视图上
    //(tips:所以如果触摸的点不在父view上,那么其上的所有子view的hitTest都不会被调用,即使clipsToBounds = NO)
    if ([self pointInside:point withEvent:event] == NO) return nil;
    
    // 3、循环遍历所有子视图,查找是否有更合适的子视图(注意:是从后向前遍历subViews)
    for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
        UIView *childView = self.subviews[i];
        //转换点到子视图坐标系上
        CGPoint childPoint = [self convertPoint:point toView:childView];
        //递归查找是否存在最合适的view
        UIView *fitView = [childView hitTest:childPoint withEvent:event];
        //如果返回非空,说明子视图中找到了最合适的view,那么返回它
        if (fitView) {
            return fitView;
        }
    }
    
    //循环结束,仍旧没有合适的子视图可以处理事件,那么就认为自己是最合适的view
    return self;
}
  • 流程图:
    image
2、事件响应(“/”过程)

在找到最合适的view之后,会调用view的 touchesXXX:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event(触摸事件)等响应方法对事件进行响应

  • 默认不处理,沿 响应者链(Responser Chain) 向上传递给下一个响应者
    • 如果下一个响应者的touches等响应方法没有重写,事件会继续沿着响应者链往上走
    • 如果传递到UIApplication,依旧不能处理事件那么事件就被丢弃

image

响应链节点表现
UIView如果view是viewcontroller的根view,那么下一个响应者是viewcontroller,否则是super view
UIViewcontroller如果viewcontroller的view是window的根view,那么下一个响应者是window;如果viewcontroller是另一个viewcontroller模态推出的,那么下一个响应者是另一个viewcontroller;如果viewcontroller的view被add到另一个viewcontroller的根view上,那么下一个响应者是另一个viewcontroller的根view
UIWindowUIWindow的下一个响应者是UIApplication
UIApplication通常UIApplication是响应者链的顶端(如果app delegate也继承了UIResponder,事件还会继续传给app delegate)
  • 响应者链构成:由一系列可以响应事件的对象(继承于UIResponder)组成
  • 响应者链的头节点:最佳响应View
  • 响应者链作用:决定了响应者对象响应事件的先后顺序,
  • 响应者链生成时机:在递归查找最合适的view的时候形成

四、实践场景

部分实例可参考:https://www.jianshu.com/p/74a2f44840fa

1、事件拦截
  • 场景:父View不想让子View感知到某些类型的事件,或父View指定响应的子View
  • 方法:重写 父ViewhitTest方法,不通过正常 遍历 寻找最佳子View,而是返回自身或指定子View
  • 原理:原最佳响应的View未被父View通过正常的 递归 hitTest 遍历到,而是直接被父View返回了 指定View(即“V”的顶点) 作为最佳响应View
图解:
                    \     ·
                     \   ·
                      \ ·
                       ·    --------父View,指定自身为最佳响应View    
                        ·   ------- 子View,无法感知事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    if ([self pointInside:point withEvent:event] == NO) return nil;
    
    //return self;                  // 情况1:返回自身,事件传递到此为止,自己就是最佳响应View
    return self.subviews[0];        // 情况2:返回指定子View,指定的View即为最佳响应的View
}
2、事件透传
  • 场景:当前View不响应事件,由父View响应
  • 方法:重写 当前ViewhitTest方法,retun nil
  • 原理:子View(原“V”的顶点,即原最佳响应View) 的hitTest返回了nil,则父View 的hitTest遍历子View未找到最佳响应的View,则return self使 自己(即父View)成为 新的最佳响应的View(新的“V”的顶点),从而会 自动调用 父View的touches方法响应
图解:
    \       ·         \     ·
     \     ·           \   ·
      \   ·     --->    \ ·  
       \ ·               ·                           --------父View,新的最佳响应View,指定自身为最佳响应View 
        ·                 ·                          ------- 子View,原最佳响应View,可感知事件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    // 可以感知到事件,但不处理,进入“/”(事件响应)流程
    return nil;
}
3、事件多层级响应
  • 场景:当前View响应事件、父View也可响应事件
  • 方法:重写 当前ViewtouchesXXX:withEvent:响应链方法,自己的事件响应完后调用 [super touchesXXX:touches withEvent:event];
  • 原理:当前View已经被确认为最佳响应View,并被回调touches方法响应,但 主动调用 父View的touches方法
图解:
    \       ·         \       ·
     \     ·           \     ·
      \   ·     --->    \   ·  
       \ ·               \ /                         --------父View,响应回调,被子View主动调用touches事件
        ·                 ·                          ------- 子View,响应回调,因是最佳响应View,所以“自动”调用touches事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"red touches begin");  //自己的处理
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"red touches moved");  //自己的处理
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"red touches end");  //自己的处理
    [super touchesBegan:touches withEvent:event];
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"red touches canceled");  //自己的处理
    [super touchesBegan:touches withEvent:event];
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值