事件响应
什么是事件?
iOS中有大量的人机交互事件,比如触摸屏幕、摇晃手机、耳机或外接手柄的操作等等。这些事情传递到手机中让手机作出响应的就是事件。
iOS中事件分为三大类:
- 触屏事件(手势:触摸、滑动、拖动…)
- 传感器事件(摇一摇、陀螺仪)
- 远程控制事件(耳机、外接手柄)
事件的响应
ios 中只有继承了 UIResponder 的对象才能接受并处理事件,这样的对象称为“响应者对象”。
UIResponder作为响应者的基类,定义了一些关于响应的属性和方法,用于子类判断或者执行关于响应的操作。iOS中有3种类继承自 UIResponder:
- UIApplication
- UIViewController
- UIView
触摸事件解析
我们在手机上的一个触摸或点击,它其实首先是手机屏幕和底层相关的硬件对这个触摸一个解析的过程,将这个触摸解析成 event,然后 iOS 系统将这个 event 事件传递到相应的界面上,由界面来响应我们的操作,给出对应的反馈。
UITouch、UIEvent、UIResponder
UITouch
当用户用手指触摸屏幕时,会创建相关的UITouch对象,当手指离开屏幕时,系统会销毁相应的UITouch对象。
UITouch 保存着跟手指相关的信息,如:触摸的位置,大小,移动和力度等
一根手指对应一个UITouch,多个手指同时点击会产生多个 UITouch,但 UIResponder 只会相应一次。
1 2 3 4 5 6
| @interface UITouch : NSObject
- (CGPoint)locationInView:(nullable UIView *)view;
- (CGPoint)previousLocationInView:(nullable UIView *)view; @end
|
UIEvent
每次产生的事件,就对应一个 UIEvent,UIEvent 来描述单个用户与APP交互的对象。APP可以接收很多不同类型的 Event。
1
| @interface UIEvent : NSObject
|
UIResponder响应者对象
UIResponder 是响应和处理事件的抽象接口。即它可以接收到UIEvent的对象,也是可以最终响应用户的操作的对象。
UIResponder中提供了以下4个对象方法来处理触摸事件:
1 2 3 4 5 6 7 8 9 10
| - (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));
|
UIGestureRecongnizer 是个手势识别器,把识别到手势事件通过 addTarget
添加到对应的相应者进行相应。
触摸事件传递过程
无法接收触摸事件的情况
事件的产生和传递
事件的产生过程:
发生触摸事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中。因为队列的特点是FIFO,先进先出,先产生的事件先处理。
UIApplication 会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口 keyWindow
keyWindow 会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步
找到合适的视图控件后,就会调用视图控件的 touches 方法来作具体的事件处理。
简单来说:触摸事件是从父控件传递到子控件,UIApplication -> window -> 寻找最合适处理事件的View
事件的传递:
keyWindow 接收到UIApplication传递的事件后,会先判断自己能否接收事件,如果能,则会判断点是否在自己身上。
如果触摸点在 keyWindow 上,keyWindow会倒序遍历子控件来寻找合适View(先遍历新添加的,效率更高)
当遍历到每个 View 时,会重复执行1、2两个步骤 (传递事件给子控件, 判断子控件能否接受事件, 触摸点是否在子控件上)
循环遍历子控件后,直到找到最合适的View,如果没有符合的子控件,则认为自己为最合适处理的View.
寻找最合适的View的过程,用到如下的两个重要方法 :
1 2 3 4 5 6 7 8 9 10 11 12
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
|
hitTest:withEvent:
方法实现的原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| -(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]) { return nil; } int count = (int)self.subviews.count; for (int i = count - 1; i >= 0; i--) { UIView *chileV = self.subviews[i]; CGPoint childP = [self convertPoint:point toView:chileV]; UIView *fitView = [chileV hitTest:childP withEvent:event]; if(fitView){ return fitView; } } return self; }
|
由上总结事件的传递顺序:
产生触摸事件 -> UIApplication事件队列 -> [UIWindow hitTest:withEvent:] -> 返回更合适的 View -> 子控件hitTest:withEvent: -> 返回最合适的View
代码验证
添加方法执行的标注:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class ZMWindow: UIWindow { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { print("\n") print(type(of: self), #function, separator: " ", terminator: " --> ") let v = super.hitTest(point, with: event) return v } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { print(type(of: self), #function) let res = super.point(inside: point, with: event) return res } }
class ZMView: UIView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { print(type(of: self), #function, separator: " ", terminator: " --> ") let v = super.hitTest(point, with: event) return v } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { print(type(of: self), #function) let res = super.point(inside: point, with: event) return res } }
class RedView: ZMView {} class YellowView: ZMView {} class BlueView: ZMView {}
|
在UIViewController中依次添加View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| override func viewDidLoad() { super.viewDidLoad() let red = RedView(frame: CGRect(x: 100, y: 100, width: 200, height: 50)) red.backgroundColor = .red let yellow = YellowView(frame: CGRect(x: 150, y: 200, width: 150, height: 50)) yellow.backgroundColor = .yellow let blue = BlueView(frame: CGRect(x: 100, y:300, width: 200, height: 50)) blue.backgroundColor = .blue view.addSubview(red) view.addSubview(yellow) view.addSubview(blue) }
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print(type(of: self), #function) }
|
最后点击VC的View,结果打印:
1 2 3 4 5 6 7 8 9 10 11
| ZMWindow hitTest(_:with:) --> ZMWindow point(inside:with:) BlueView hitTest(_:with:) --> BlueView point(inside:with:) YellowView hitTest(_:with:) --> YellowView point(inside:with:) RedView hitTest(_:with:) --> RedView point(inside:with:)
ZMWindow hitTest(_:with:) --> ZMWindow point(inside:with:) BlueView hitTest(_:with:) --> BlueView point(inside:with:) YellowView hitTest(_:with:) --> YellowView point(inside:with:) RedView hitTest(_:with:) --> RedView point(inside:with:) MainViewController touchesBegan(_:with:)
|
通过上述验证,我们可以看到,为什么 VC 会进行2次寻找最适合的 View,然后再自己响应触摸事件?
触摸事件响应过程
1、事件传递找到了最合适的 View,最合适的 View 会调用 touches
的几个方法进行具体的事件处理,如果合适 View 重写了 touches
方法,则会进行事件的处理。否则系统调用该方法的默认实现。
2、Apple 官方文档上说 UIView 的 touches
方法,默认不处理事件, 它会将事件顺着响应链向上转发给上一个 UIResponder 处理,如果找到合适的 View 就会调用该view的touches方法要进行响应处理具体的事件,找不到就不会调用。
响应者链条
响应者链条是由多个响应者对象(UIResponder类型)一起组合起来的链条。事件在响应者链条中传递的过程:
1、如果当前 View 是控制器的 View,那么控制器是上一个响应者,事件就会传递给控制器。如果当前 View 不是控制器的View,那么当前View 的父视图就是上一个响应者,事件会传递给父视图。
2、事件会从合适的View的视图层级中向上传递,如果顶层View也无法处理,则事件最终传递给 UIWindow 处理
3、如果 UIWindow 也无法处理,则事件会传递给 UIApplication 处理
4、如果UIApplication无法处理,则该事件会被丢弃。
触摸事件的完整过程总结
1、用户触摸屏幕后会产生触摸事件,触摸事件会被添加到 UIApplication 管理的事件队列中。
2、UIApplication 会从事件队列中取出最前面的事件,把事件出递给 keyWindow
3、keyWindow会在视图层级中找到最合适的 View 来处理触摸事件
4、最合适的View会调用自己的touches方法进行事件处理
5、touches方法会顺着响应者链条向上传递,直到找到处理事件的视图,或者最终被 UIApplication抛弃。
事件响应的应用
找到 View 所属的 VC
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @implementation UIView (Extension) -(UIViewController*)parentController { UIResponder *responder = [self nextResponder]; while (responder) { if ([responder isKindOfClass:[UIViewController class]]) { return (UIViewController*)responder; } responder = [responder nextResponder]; } return nil; } @end
|
一个事件被多个View响应
点击 yellow 时,red 的touches 方法也会被响应。
因为事件的响应是顺着响应者链条向上传递的, 即从子控件传递给父控件, touch方法默认不处理事件, 而是把事件顺着响应者链条传递给上一个响应者.
1 2 3 4 5 6 7 8 9 10 11 12
| class RedView: UIView { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print(type(of: self), #function) } }
class YellowView: UIView { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print(type(of: self), #function) super.touchesBegan(touches, with: event) } }
|
1 2 3 4 5 6 7 8 9 10 11
| override func viewDidLoad() { super.viewDidLoad()
let red = RedView(frame: CGRect(x: 100, y: 250, width: 100, height: 100)) red.backgroundColor = .red let yellow = YellowView(frame: CGRect(x: 50, y: 50, width: 50, height: 50)) yellow.backgroundColor = .yellow view.addSubview(red) red.addSubview(yellow) }
|
扩大UIView的点击范围
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class YellowView: UIView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !isUserInteractionEnabled || isHidden || alpha <= 0.01 { return nil } let touchRect = bounds.insetBy(dx: -10, dy: -10) if touchRect.contains(point) { for subView in subviews.reversed() { let convertedPoint = (subView as UIView).convert(point, to: self) let hitTestView = (subView as UIView).hitTest(convertedPoint, with: event) if let hitTestView = hitTestView { return hitTestView } } return self } return nil } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { print("点击了YellowView") } }
|
视图被遮挡时响应
YellowView 遮挡了 greenBtn,点击YellowView 时,让 greenBtn 响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class YellowView: UIView { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let hitView = super.hitTest(point, with: event) if hitView === self { return nil } return hitView } }
let btn = UIButton(type: .custom) btn.backgroundColor = .green btn.frame = CGRect(x: 0, y: 0, width: 60, height: 60) btn.center = view.center btn.addTarget(self, action: #selector(clickGreenBtn), for: .touchUpInside) view.addSubview(btn) let yellow = YellowView(frame: CGRect(x: 0, y: 0, width: 50, height: 50)) yellow.backgroundColor = .yellow yellow.center = self.view.center self.view.addSubview(yellow)
|
学习博客
HChong
zhangzr’s blog
史上最详细的iOS之事件的传递和响应机制-原理篇
子控件超出父控件响应点击事件
FrizzleFur/DailyLearning