iOS的事件分为3大类型

  • Touch Events(触摸事件)
  • Motion Events(运动事件,比如重力感应和摇一摇等)
  • Remote Events(远程事件,比如用耳机上得按键来控制手机)

在开发中,最常用到的就是Touch Events(触摸事件),基本贯穿于每个App中,也是本文的猪脚~ 因此文中所说事件均特指触摸事件。

接下来,记录、涉及的问题大致包括:

  • 事件是怎么找它的妈妈的?(寻找事件的最佳响应者)
  • 事件又是如何去到妈妈的身边的?妈妈又将如何对待它?(事件的响应及在响应链中的传递)

寻找事件的最佳响应者(Hit-Testing)

当我们触摸屏幕的某个可响应的功能点后,最终都会由UIView或者继承UIView的控件来响应

那我们先来看下UIView的两个方法:

 // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//返回寻找到的最终响应这个事件的视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;  // default returns YES if point is in bounds
//判断某一个点击的位置是否在视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

每个UIView对象都有一个 hitTest: withEvent: 方法,这个方法是Hit-Testing过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁。

看看它是什么时候被调用的

  • 当手指接触屏幕,UIApplication接收到手指的触摸事件之后,就会去调用UIWindowhitTest: withEvent:方法
  • hitTest: withEvent:方法中会调用pointInside: withEvent:去判断当前点击的point是否属于UIWindow范围内,如果是,就会以倒序的方式遍历它的子视图,即越后添加的视图,越先遍历
  • 子视图也调用自身的hitTest: withEvent:方法,来查找最终响应的视图

再来看个示例:

视图层级如下(同一层级的视图越在下面,表示越后添加):

A
├── B
│   └── D
└── C├── E└── F

现在假设在E视图所处的屏幕位置触发一个触摸,App接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:

  • UIWindow将事件传递给其子视图A
  • A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。
  • C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。
  • F判断自身不能响应事件,C又将事件传递给E。
  • E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者。

以上,就是寻找最佳响应者的整个过程。

接下来,来看下hitTest: withEvent:方法里,都做些了什么?

我们已经知道事件在响应者之间的传递,是视图通过判断自身能否响应事件来决定是否继续向子视图传递,那么判断响应的条件是什么呢?

视图响应事件的条件:

  • 允许交互: userInteractionEnabled = YES
  • 禁止隐藏:hidden = NO
  • 透明度:alpha > 0.01
  • 触摸点的位置:通过 pointInside: withEvent:方法判断触摸点是否在视图的坐标范围内

代码的表现大概如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {//3种状态无法响应事件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;
}

说了这么多,那我们可以运用hitTest: withEvent:来搞些什么事情呢

使超出父视图坐标范围的子视图也能响应事件

视图层级如下:

A
├── B

如上图所示,视图B有一部分是不在父视图A的坐标范围内的,当我们触摸视图B的上半部分,是不会响应事件的。当然,我们可以通过重写视图AhitTest: withEvent:方法来解决这个需求。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {UIView *view = [super hitTest:point withEvent:event];//如果找不到合适的响应者if (view == nil) {//视图B坐标系的转换CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {// 满足条件,返回视图Bview = self.deleteButton;}}return view;
}

视图AhitTest: withEvent:方法中判断触摸点,是否位于视图B的视图范围内,如果属于,则返回视图B。这样一来,当我们点击视图B的任何位置都可以响应事件了。

事件的响应及在响应链中的传递

经历Hit-Testing后,UIApplication已经知道事件的最佳响应者是谁了,接下来要做的事情就是:

  • 将事件传递给最佳响应者响应
  • 事件沿着响应链传递

事件传递给最佳响应者

最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。

UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给最佳响应者。

寻找事件的最佳响应者一节中点击视图E为例,在EViewtouchesBegan:withEvent: 上打个断点查看调用栈就能看清这一过程:

当事件传递给最佳响应者后,响应者响应这个事件,则这个事件到此就结束了,它会被释放。假设响应者没有响应这个事件,那么它将何去何从?事件将会沿着响应链自上而下传递。

注意: 寻找最佳响应者一节中也说到了事件的传递,与此处所说的事件的传递有本质区别。上面所说的事件传递的目的是为了寻找事件的最佳响应者,是自下而上(父视图到子视图)的传递;而这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下(子视图到父视图)的。前者为“寻找”,后者为“响应”。

事件沿着响应链传递

在UIKit中有一个类:UIResponder,它是所有可以响应事件的类的基类。来看下它的头文件的几个属性和方法

NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif--------------省略部分代码------------// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application.  Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (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);

UIApplication,UIViewController和UIView都是继承自它,都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者,也通过nextResponder来串成响应链

在App中,所有的视图都是根据树状层次结构组织起来的,因此,每个View都有自己的SuperView。当一个ViewaddSuperView上的时候,它的nextResponder属性就会被指向它的SuperView,各个不同响应者的指向如下:

  • UIView 若视图是控制器的根视图,则其nextResponder为控制器对象;否则,其nextResponder为父视图。
  • UIViewController 若控制器的视图是window的根视图,则其nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。
  • UIWindow nextResponder为UIApplication对象。
  • UIApplication 若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的nextResponder为app delegate。

这样,整个App就通过nextResponder串成了一条链,也就是我们所说的响应链,子视图指向父视图构成的响应链。

看一下官网对于响应链的示例展示

若触摸发生在UITextField上,则事件的传递顺序是:

  • UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegte

图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponderUIViewController对象;若是直接addUIWindow上的,则其nextResponderUIWindow对象。

响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent: 方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。

响应者对于接收到的事件有3种操作:

  • 不拦截,默认操作 事件会自动沿着默认的响应链往下传递
  • 拦截,不再往下分发事件 重写 touchesBegan:withEvent: 进行事件处理,不调用父类的 touchesBegan:withEvent:
  • 拦截,继续往下分发事件 重写 touchesBegan:withEvent: 进行事件处理,同时调用父类的 touchesBegan:withEvent: 将事件往下传递

因此,你也可以通过 touchesBegan:withEvent:方法搞点事情~

总结

触摸事件先通过自下而上(父视图–>子视图)的传递方式寻找最佳响应者,

然后以自上而下(子视图–>父视图)的方式在响应链中传递。

【iOS面试粮食】UI视图—iOS事件的传递机制相关推荐

  1. 详解Android Touch事件的传递机制

    1.基础知识 (1) 所有Touch事件都被封装成了MotionEvent对象,包括Touch的位置.时间.历史记录以及第几个手指(多指触摸)等. (2) 事件类型分为ACTION_DOWN, ACT ...

  2. iOS面试准备 - ios篇

    iOS面试准备 - ios篇 ios面试准备 - objective-c篇 ios面试准备 - 网络篇 IOS面试准备 - C++篇 iOS面试准备 - 其他篇 运行时 https://juejin. ...

  3. 过招多家大厂提炼的iOS面试心经

    2020年,整个资本市场风起云涌,大环境下,互联网更是风声鹤唳,大多数公司面临着裁员,结构重构,他们收紧资本,为自己取暖.在漫长的寒冬下,互联网人只有自己修炼内功,才能在寒风中屹立不倒. 作为一名iO ...

  4. ios面试准备 - 网络篇

    iOS面试准备 - ios篇 ios面试准备 - objective-c篇 ios面试准备 - 网络篇 IOS面试准备 - C++篇 iOS面试准备 - 其他篇 http和https是什么?http和 ...

  5. Android 中Touch(触屏)事件传递机制

    版本:2.0 日期:2014.3.21 2014.3.29 版权:© 2014 kince 转载注明出处 一.基本概念 在实际开发中,经常会遇到与触屏事件有关的问题,最典型的一个就是滑动冲突.比如在使 ...

  6. Android 触摸事件(Touch)的传递机制

    Touch 事件的传递机制 一个完整的touch 事件,由一个 down 事件.n 个 move 事件,一个 up 事件组成. Touch 事 件 一 般 的 传 递 流 程 Activity--&g ...

  7. Android事件分发之ACTION_MOVE与ACTION_UP的传递机制

    目录 引言 ACTION_MOVE与ACTION_UP的传递机制 mFirstTouchTarget作用 mFirstTouchTarget为什么是链表结构 引言 关于Android事件分发机制网上相 ...

  8. iOS面试题-UI篇

    http://www.360doc.com/content/20/0630/21/31460730_921535347.shtml 这里有关于面试方面汇总:关于iOS面试题汇总(栏目持续更新)http ...

  9. iOS面试攻略,你必须拥有

    还在面试的时候感觉自己像一只无头苍蝇么?本文为大家整理了一系列iOS面试题,其中包括一些Objective-C的关键字和概念,少编也祝各位马到功成. @ 看到这个关键字,我们就应该想到,这是Objec ...

最新文章

  1. mysql中uuid的写法_MySQL IS_UUID()用法及代码示例
  2. MM模块几个移动类型之间的区别
  3. 用C#快速往Excel写数据
  4. AOP(基于注解对AspectJ操作)
  5. 我只注视你全cg存档_在暴戾的他怀里撒个娇 作者:春风榴火全娱乐圈都在等我们离婚作者:魔安...
  6. atoi 原来将字符串02002xzm100转换为int以后是2002
  7. day07【后台】SpringSecurity
  8. 点云nurbs曲面重建c++代码_【科普】抢先收藏!点云数据处理技术概要
  9. 如何在Android中的ListView中延迟加载图像
  10. Linux文件扩展思考随笔
  11. Oracle自定义函数示例
  12. atitit.html编辑器的设计要点与框架选型 attilax总结
  13. HTML生日快乐代码
  14. 什么时候做牙齿矫正好呢?
  15. 微信小程序登陆(两种写法)
  16. 电力猫服务器的网页,电力猫怎么配对?快速配置电力猫的图文教程
  17. 【spring】 官网文档手册(附中文网址)
  18. 最经典java使用Jedis操作Redis
  19. 六、数据(分组)计算
  20. win10家庭版升级专业版

热门文章

  1. 输电线路杆塔倾斜在线监测系统
  2. 使用idsdt制作生成显卡代码的dsdt文件驱动显卡
  3. python海龟图画koch雪花曲线_python画图——雪花(科赫曲线)
  4. 世上最杰出程序员,B 语言、Unix 之父嫌计算机发展太慢,让孩子学生物?
  5. 地球大气层简介与垂直分层
  6. keytool用法(一)
  7. dfs 牛客 迷宫问题
  8. 《金融数据分析导论:基于R语言》习题答案(第一章)
  9. 微信小程序开发之图片压缩方案
  10. 高等几何——射影变换6