当前位置: 首页 > news >正文

[iOS开发]事件处理与响应者链

响应链

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

响应者

在iOS中,能够响应事件的对象都是UIResponder的子类对象。当事件来到时,系统会将事件传递给合适的响应者,并且将其成为第一响应者。第一响应者未处理的事件,将会在响应者链中进行传递,传递规则由UIResponder的nextResponder决定,可以通过重写该属性来决定传递规则。当一个事件到来时,第一响应者没有接收消息,则顺着响应者链向后传递。

UIResponder、UIEvent 和 UIControl的介绍、联系与区别

UIResponder

@interface UIApplication : UIResponder
@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate>

我们所熟悉的UIApplicationUIViewUIViewController这几个类是直接继承自UIResponderUIResponder类是专门用来响应用户的操作处理各种事件(UIEvent)的。
UIResponder提供了用户点击、按压检测(presses)以及手势检测(motion)的回调方法,分别对应用户开始、移动、结束以及取消,其中只有在程序强制退出或者来电时,取消事件才会调用。
我们可以看一下UIResponder的声明:

@interface UIResponder : NSObject <UIResponderStandardEditActions>

@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

@property(nonatomic, readonly) BOOL canBecomeFirstResponder;    // default is NO
- (BOOL)becomeFirstResponder;

@property(nonatomic, readonly) BOOL canResignFirstResponder;    // default is YES
- (BOOL)resignFirstResponder;

@property(nonatomic, readonly) BOOL isFirstResponder;

- (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 API_AVAILABLE(ios(9.1));

- (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 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));

...

@end

我们可以试着玩一下,touches系列就是手指的点击事件会触发,motion系列就是加速计事件(比如摇一摇手机等)press在官方文档里说是related to the press of a physical button和物理按键有关,但我自己没试出来。

UIEvent

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,
};

是由硬件捕获到的一个表示用户操作设备的对象,事件主要分为三类:包括触摸事件(Touch Events对应就是UITouch)、运动事件(Motion Events)、远程控制事件(Remote Control Events),通过代码,发现新版本还可以有按压事件、滑动事件、悬停事件、变换事件。官方文档里面没有说明这几个,那我先暂不深究。

UIControl

UIControlUIView的子类,当然也是UIResponder的子类。UIControl是诸如UIButtonUISwitchUITextField等控件的父类,它本身也包含了一些属性和方法,但是不能直接使用UIControl类,它只是定义了子类都需要使用的方法,我们只会使用它的子类。
如果说UIResponder 实例对象可以对随机事件进行响应并处理,那么UIEvent 代表一个单一并只含有一种类型的事件,这个类型可以是触摸、远程控制或者按压,对应的子类具体一点可能是设备的摇动(为了处理系统事件,UIResponder 的子类可以通过重写一些对应的方法从而让它们可处理具体的 UIEvent 类型)。
在某种程度上,你可以将 UIEvents 视为通知。虽然 UIEvents 可以被子类化并且 sendEvent 可以被手动调用,但它们并不真正意味着可以这么做,至少不是通过正常方式。由于你无法创建自定义类型,派发自定义事件会出现问题,因为非预期的响应者可能会错误地 “处理” 你的事件。尽管如此,你仍然可以使用它们,除了系统事件,UIResponder 还可以以 Selector 的形式响应任意 “事件”。
虽然 UIResponder 可以完全检测触摸事件,但如何区分不同类型的触摸事件呢?
这就是 UIControl 擅长的地方,UIControl相当于就是对UIResponder进行了一次封装,已经将手势与View进行了封装绑定,比如我们的UIButton为什么可以检测到双击,单击等等操作,也都是在UIControl里写好的。

typedef NS_OPTIONS(NSUInteger, UIControlState) {
    UIControlStateNormal       = 0,
    UIControlStateHighlighted  = 1 << 0,                  // used when UIControl isHighlighted is set
    UIControlStateDisabled     = 1 << 1,
    UIControlStateSelected     = 1 << 2,                  // flag usable by app (see below)
    UIControlStateFocused API_AVAILABLE(ios(9.0)) = 1 << 3, // Applicable only when the screen supports focus
    UIControlStateApplication  = 0x00FF0000,              // additional flags available for application use
    UIControlStateReserved     = 0xFF000000               // flags reserved for internal framework use
};
typedef NS_OPTIONS(NSUInteger, UIControlEvents) {
    UIControlEventTouchDown                                         = 1 <<  0,      // on all touch downs
    UIControlEventTouchDownRepeat                                   = 1 <<  1,      // on multiple touchdowns (tap count > 1)
    UIControlEventTouchDragInside                                   = 1 <<  2,
    UIControlEventTouchDragOutside                                  = 1 <<  3,
    UIControlEventTouchDragEnter                                    = 1 <<  4,
    UIControlEventTouchDragExit                                     = 1 <<  5,
    UIControlEventTouchUpInside                                     = 1 <<  6,
    UIControlEventTouchUpOutside                                    = 1 <<  7,
    UIControlEventTouchCancel                                       = 1 <<  8,

    UIControlEventValueChanged                                      = 1 << 12,     // sliders, etc.
    UIControlEventPrimaryActionTriggered API_AVAILABLE(ios(9.0))    = 1 << 13,     // semantic action: for buttons, etc.
    UIControlEventMenuActionTriggered API_AVAILABLE(ios(14.0))      = 1 << 14,     // triggered when the menu gesture fires but before the menu presents

    UIControlEventEditingDidBegin                                   = 1 << 16,     // UITextField
    UIControlEventEditingChanged                                    = 1 << 17,
    UIControlEventEditingDidEnd                                     = 1 << 18,
    UIControlEventEditingDidEndOnExit                               = 1 << 19,     // 'return key' ending editing

    UIControlEventAllTouchEvents                                    = 0x00000FFF,  // for touch events
    UIControlEventAllEditingEvents                                  = 0x000F0000,  // for UITextField
    UIControlEventApplicationReserved                               = 0x0F000000,  // range available for application use
    UIControlEventSystemReserved                                    = 0xF0000000,  // range reserved for internal framework use
    UIControlEventAllEvents                                         = 0xFFFFFFFF
};

事件的产生、传递和响应过程

UIApplication–>UIWindow–>递归找到最合适处理的控件–>控件调用 touches 方法–>判断是否实现 touches 方法–>没有实现默认会将事件传递给上一个响应者–>找到上一个响应者–>找不到方法作废。

事件的产生和传递过程

  • 当触摸事件发生时,压力转为电信号,iOS系统将产生UIEvent对象,记录事件产生的事件和类型,然后系统将事件加入到一个由UIApplication管理的事件队列中。
  • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)
  • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件
  • 找到合适的视图控件后,就会调用视图控件的touches方法来作事件的具体处理:touchesBegin··· touchesMoved··· touchesEnded···
  • 这些touches方法默认的做法是将事件顺着响应链条向上传递,将事件交给上一个响应者处理

一般事件的传递是从父控件传递到子控件的,如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件。
UIView不能接收触摸事件的三种情况:

  • 不接受用户交互:userInteractionEnabled = NO;
  • 隐藏: hidden= YES;
  • 透明: alpha = 0.0 ~0.01

hit-test

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

先看一下官方文档的解释:

This method traverses the view hierarchy by calling the pointInside:withEvent: method of each subview to determine which subview should receive a touch event. If pointInside:withEvent: returns YES, then the subview’s hierarchy is similarly traversed until the frontmost view containing the specified point is found. If a view does not contain the point, its branch of the view hierarchy is ignored. You rarely need to call this method yourself, but you might override it to hide touch events from subviews.
此方法通过调用每个子视图的 pointInside:withEvent:方法来遍历视图层次结构,以确定哪个子视图应接收触摸事件。如果 pointInside:withEvent:返回 YES,则子视图的层次结构将以类似的方式遍历,直到找到包含指定点的最前面视图。如果视图不包含该点,则忽略其视图层次结构的分支。您很少需要自己调用此方法,但您可以覆盖它以隐藏子视图中的触摸事件。
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.
Points that lie outside the receiver’s bounds are never reported as hits, even if they actually lie within one of the receiver’s subviews. This can occur if the current view’s clipsToBounds property is set to NO and the affected subview extends beyond the view’s bounds.
此方法忽略隐藏的视图对象、已禁用用户交互或透明度小于 0.01 的视图对象。此方法在确定匹配时不考虑视图的内容。因此,即使指定的点位于该视图内容的透明部分中,仍可以返回视图。
位于接收方边界之外的点永远不会被报告为命中,即使它们实际上位于接收方的某个子视图中。如果当前视图的 clipsToBounds 属性设置为 NO,并且受影响的子视图超出视图的边界,则可能会发生这种情况。

用户的触摸事件首先会由系统截获,进行包装处理等。然后递归遍历所有的view,进行碰触测试(hitTest),直到找到可以处理事件的view
Hit-Testing 先检查触摸对象所在的位置是否在对应任意屏幕上的视图对象的区域范围内。如果在的话,就开始对此视图对象的子视图对象进行同样的检查。视图树中最底层那个包含此触摸点位置的视图对象,就是要查找的 hit-test 视图对象。iOS 一旦确定 hit-test 视图对象,就会把触摸事件传递给它进行处理。
对于下图这种情况:
请添加图片描述
请添加图片描述

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"进入A_View---hitTest withEvent ---");
    UIView * view = [super hitTest:point withEvent:event];
    NSLog(@"离开A_View--- hitTest withEvent ---hitTestView:%@",view);
    return view;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    NSLog(@"A_view--- pointInside withEvent ---");
    BOOL isInside = [super pointInside:point withEvent:event];
    NSLog(@"A_view--- pointInside withEvent --- isInside:%d",isInside);
    return isInside;
}

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

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

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

我们在里面都这样重写方法。点击E试一下:
请添加图片描述
再点一下B试一下:
请添加图片描述
首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内,若pointInside:withEvent:方法返回NO,说明触摸点不在当前视图内,则当前视图的hitTest:withEvent:返回nil。若pointInside:withEvent:方法返回YES,说明触摸点在当前视图内,则遍历当前视图的所有子视图,调用子视图的hitTest:withEvent:方法重复前面的步骤,子视图的遍历顺序是从topbottom,即从subviews数组的末尾向前遍历,直到有子视图的hitTest:withEvent:方法返回非空对象或者全部子视图遍历完毕。
UIApplication对象维护着自己的一个响应者栈,当pointInSide: withEvent:返回YES的时候,响应者入栈。传递链中是没有 controller 的,因为 controller 本身不具有大小的概念。但是响应链中是有 controller 的,因为 controller 继承自 UIResponder。所以controller可能是个单独的例外,其不需要pointInside方法就可以自己进入响应者栈。
若第一次有子视图的hitTest:withEvent:方法返回非空对象,则当前视图的hitTest:withEvent:方法就返回此对象,处理结束之后,若所有子视图的hitTest:withEvent:方法都返回nil,则当前视图的hitTest:withEvent:方法返回当前视图自身。
hitTest内部实现过程:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

响应过程

响应者

响应者是可以响应事件并处理它们的对象。所有响应者对象都是最终从 UIResponder(iOS)继承的类的实例。这些类声明了一个用于事件处理的编程接口,并定义了响应者的默认行为。应用程序的可见对象几乎总是响应者(例如,windows、views 和 controls),而应用程序对象(AppDelegate)也是响应者。在 iOS 中,视图控制器(UIViewController 对象)也是响应者对象。
若要接收事件,响应者必须实现适当的事件处理方法,在某些情况下,告诉应用它可以成为第一响应者。

响应者链

响应者链其实就是很多响应者对象(继承自UIResponder的对象)一起组合起来的链条称之为响应者链条。
如果第一个响应者无法处理事件或 action消息,它会将其转发给一个称为响应者链的链接系列中的 “下一个响应者” (next responder)。响应者链允许响应者对象将处理事件或 action 消息的责任转移到应用程序中的其他对象。如果响应者链中的对象无法处理事件或 action,它会将消息传递给链中的下一个响应者。消息沿着链向上传播,指向更高级别的对象,直到被处理为止。如果未处理,应用程序将丢弃它。
请添加图片描述
响应者链是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过 UIResponder 的属性串联起来的。

@property(nonatomic, readonly, nullable) UIResponder *nextResponder;

响应过程

如果没有实现touches方法那么一般默认做法是控件将事件顺着响应者链向上传递,将事件交给上一个响应者进行处理。那么如何判断当前响应者的上一个响应者是谁呢?

  • 判断当前是否是控制器的 view,如果是控制器的 view,上一个响应者就是控制器
  • 如果不是控制器的 view,上一个响应者就是父控件

当有 view 能够处理触摸事件后,开始响应事件。系统会调用view的以下方法:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

可以使多个对象共同响应同一个事件。只需要在以上方法重载中调用super的方法。
大致的过程initial view -> super view -> ···->view controller -> window -> Application

实际应用

扩大button点击范围

重写pointInside方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect bounds = CGRectMake(-10, -10, self.bounds.size.width + 20, self.bounds.size.height + 20);
    return CGRectContainsPoint(bounds, point);
}

请添加图片描述
红色:按钮大小
蓝色:扩大点击区域

重写HitTest方法

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect bounds = CGRectMake(-10, -10, self.bounds.size.width + 20, self.bounds.size.height + 20);
    if (CGRectContainsPoint(bounds, point)) {
        return self;
    } else {
        return nil;
    }
    return self;
}

点击穿透

请添加图片描述
视图继承关系示意图:
请添加图片描述
目的:点击红色与蓝色重叠区域响应蓝色事件。
重写红色按钮的hitTest方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint convertedPoint = [self convertPoint:point toView:_lowwer];
    if ([_lowwer pointInside:convertedPoint withEvent:event]) {
        return _lowwer;
    }
    return self;
}

相关文章:

  • 【CSDN线上竞赛第六期竞赛 】参赛介绍
  • 各种场景下的Git管理方法
  • 逻辑漏洞——权限控制问题
  • golang常用库之-配置文件解析 spf13/viper包 | 解析加载配置
  • Rust(7):结构体类型
  • 通信原理学习笔记6-3:数字解调——判决和误码率推导
  • Mybatis快速上手2——通用的CRUD操作
  • 基于Oracle数据库高校学生宿舍管理系统
  • 基于ACS40核心板的串口图传设计
  • Android移动应用开发之ListView和RecyclerView的简单使用
  • 探究linux进程调度
  • android新版本适配-android13最全适配方案
  • 【mongo 系列】聚合知识点梳理
  • 2022年9月26日--10月2日(ue4热更新视频教程为主)
  • 阿里云SLB负载均衡理论与操作
  • 收藏网友的 源程序下载网
  • [分享]iOS开发-关于在xcode中引用文件夹右边出现问号的解决办法
  • [译]如何构建服务器端web组件,为何要构建?
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • Cumulo 的 ClojureScript 模块已经成型
  • javascript 哈希表
  • Java的Interrupt与线程中断
  • Java读取Properties文件的六种方法
  • leetcode386. Lexicographical Numbers
  • node入门
  • Objective-C 中关联引用的概念
  • python docx文档转html页面
  • 对话 CTO〡听神策数据 CTO 曹犟描绘数据分析行业的无限可能
  • 猴子数据域名防封接口降低小说被封的风险
  • 缓存与缓冲
  • 机器学习学习笔记一
  • 经典排序算法及其 Java 实现
  • 一些css基础学习笔记
  • 06-01 点餐小程序前台界面搭建
  • 带你开发类似Pokemon Go的AR游戏
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • ​LeetCode解法汇总518. 零钱兑换 II
  • #快捷键# 大学四年我常用的软件快捷键大全,教你成为电脑高手!!
  • $refs 、$nextTic、动态组件、name的使用
  • (14)Hive调优——合并小文件
  • (6)添加vue-cookie
  • (LeetCode 49)Anagrams
  • (二)c52学习之旅-简单了解单片机
  • (附源码)spring boot火车票售卖系统 毕业设计 211004
  • (欧拉)openEuler系统添加网卡文件配置流程、(欧拉)openEuler系统手动配置ipv6地址流程、(欧拉)openEuler系统网络管理说明
  • (转)真正的中国天气api接口xml,json(求加精) ...
  • (转载)深入super,看Python如何解决钻石继承难题
  • .gitattributes 文件
  • .mysql secret在哪_MYSQL基本操作(上)
  • .NET Core IdentityServer4实战-开篇介绍与规划
  • .Net Core与存储过程(一)
  • .NET delegate 委托 、 Event 事件
  • .net操作Excel出错解决
  • .net开源工作流引擎ccflow表单数据返回值Pop分组模式和表格模式对比
  • @RequestParam,@RequestBody和@PathVariable 区别