【iOS面试粮食】UI视图—iOS事件的传递机制
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接收到手指的触摸事件之后,就会去调用UIWindow的
hitTest: 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的上半部分,是不会响应事件的。当然,我们可以通过重写视图A的 hitTest: 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;
}
在视图A的hitTest: withEvent:
方法中判断触摸点,是否位于视图B的视图范围内,如果属于,则返回视图B。这样一来,当我们点击视图B的任何位置都可以响应事件了。
事件的响应及在响应链中的传递
经历Hit-Testing后,UIApplication已经知道事件的最佳响应者是谁了,接下来要做的事情就是:
- 将事件传递给最佳响应者响应
- 事件沿着响应链传递
事件传递给最佳响应者
最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。
UIApplication中有个sendEvent:
的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给最佳响应者。
以寻找事件的最佳响应者一节中点击视图E为例,在EView的 touchesBegan: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。当一个View被add到SuperView上的时候,它的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根视图存在的,则其nextResponder
为UIViewController对象;若是直接add在UIWindow上的,则其nextResponder
为UIWindow对象。
响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent:
方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。
响应者对于接收到的事件有3种操作:
- 不拦截,默认操作 事件会自动沿着默认的响应链往下传递
- 拦截,不再往下分发事件 重写
touchesBegan:withEvent:
进行事件处理,不调用父类的touchesBegan:withEvent:
- 拦截,继续往下分发事件 重写
touchesBegan:withEvent:
进行事件处理,同时调用父类的touchesBegan:withEvent:
将事件往下传递
因此,你也可以通过 touchesBegan:withEvent:
方法搞点事情~
总结
触摸事件先通过自下而上(父视图–>子视图)的传递方式寻找最佳响应者,
然后以自上而下(子视图–>父视图)的方式在响应链中传递。
【iOS面试粮食】UI视图—iOS事件的传递机制相关推荐
- 详解Android Touch事件的传递机制
1.基础知识 (1) 所有Touch事件都被封装成了MotionEvent对象,包括Touch的位置.时间.历史记录以及第几个手指(多指触摸)等. (2) 事件类型分为ACTION_DOWN, ACT ...
- iOS面试准备 - ios篇
iOS面试准备 - ios篇 ios面试准备 - objective-c篇 ios面试准备 - 网络篇 IOS面试准备 - C++篇 iOS面试准备 - 其他篇 运行时 https://juejin. ...
- 过招多家大厂提炼的iOS面试心经
2020年,整个资本市场风起云涌,大环境下,互联网更是风声鹤唳,大多数公司面临着裁员,结构重构,他们收紧资本,为自己取暖.在漫长的寒冬下,互联网人只有自己修炼内功,才能在寒风中屹立不倒. 作为一名iO ...
- ios面试准备 - 网络篇
iOS面试准备 - ios篇 ios面试准备 - objective-c篇 ios面试准备 - 网络篇 IOS面试准备 - C++篇 iOS面试准备 - 其他篇 http和https是什么?http和 ...
- Android 中Touch(触屏)事件传递机制
版本:2.0 日期:2014.3.21 2014.3.29 版权:© 2014 kince 转载注明出处 一.基本概念 在实际开发中,经常会遇到与触屏事件有关的问题,最典型的一个就是滑动冲突.比如在使 ...
- Android 触摸事件(Touch)的传递机制
Touch 事件的传递机制 一个完整的touch 事件,由一个 down 事件.n 个 move 事件,一个 up 事件组成. Touch 事 件 一 般 的 传 递 流 程 Activity--&g ...
- Android事件分发之ACTION_MOVE与ACTION_UP的传递机制
目录 引言 ACTION_MOVE与ACTION_UP的传递机制 mFirstTouchTarget作用 mFirstTouchTarget为什么是链表结构 引言 关于Android事件分发机制网上相 ...
- iOS面试题-UI篇
http://www.360doc.com/content/20/0630/21/31460730_921535347.shtml 这里有关于面试方面汇总:关于iOS面试题汇总(栏目持续更新)http ...
- iOS面试攻略,你必须拥有
还在面试的时候感觉自己像一只无头苍蝇么?本文为大家整理了一系列iOS面试题,其中包括一些Objective-C的关键字和概念,少编也祝各位马到功成. @ 看到这个关键字,我们就应该想到,这是Objec ...
最新文章
- mysql中uuid的写法_MySQL IS_UUID()用法及代码示例
- MM模块几个移动类型之间的区别
- 用C#快速往Excel写数据
- AOP(基于注解对AspectJ操作)
- 我只注视你全cg存档_在暴戾的他怀里撒个娇 作者:春风榴火全娱乐圈都在等我们离婚作者:魔安...
- atoi 原来将字符串02002xzm100转换为int以后是2002
- day07【后台】SpringSecurity
- 点云nurbs曲面重建c++代码_【科普】抢先收藏!点云数据处理技术概要
- 如何在Android中的ListView中延迟加载图像
- Linux文件扩展思考随笔
- Oracle自定义函数示例
- atitit.html编辑器的设计要点与框架选型 attilax总结
- HTML生日快乐代码
- 什么时候做牙齿矫正好呢?
- 微信小程序登陆(两种写法)
- 电力猫服务器的网页,电力猫怎么配对?快速配置电力猫的图文教程
- 【spring】 官网文档手册(附中文网址)
- 最经典java使用Jedis操作Redis
- 六、数据(分组)计算
- win10家庭版升级专业版