【iOS开发】——事件传递链和事件响应链
- 事件链
- 事件传递链
- 事件响应链
- 点击穿透事件
- 扩大点击区域
事件链
事件链有事件传递链和事件响应链
用户点击屏幕时,首先 UIApplication 对象
先收到该点击事件,再依次传递给它上面的所有子 view
,直到传递到最上层。即由系统向最上层 view 传递,Application
-> window
-> root view
-> sub view
-> … -> first view
即传递链
。
那响应链呢,和传递链相反,由最基础的 view
向系统传递
,first view
-> super view
-> … -> view controller
-> window
-> Application
-> AppDelegate
即响应链。
总结一下,事件链包含传递链和响应链,事件通过传递链传递上去,通过响应链找到相应的 UIResponse。
事件传递链
只有继承了 UIResponser
的对象才能够接受处理事件。UIResponse
是响应对象的基类
,定义了处理各种事件的接口。在 UIKit
中我们使用响应者对象 Responder
接收和处理事件。一个响应者对象一般是 UIResponder
类的实例,它常见的子类包括 UIView
,UIViewController
和 UIApplication
,这意味着几乎所有我们日常使用的控件都是响应者,如 UIButton
,UILabel
等等。
在 UIResponder
及其子类中我们通过UITouch
来处理和传递事件UIEvent
的,也就是最开始说的点击屏幕,具体有以下几种:
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
在 UITouch
内,存储了大量触摸相关的数据,当手指在屏幕上移动时,所对应的 UITouch 数据也会更新,这也是我们手机操作起来很丝滑的一个原因,另外需要注意的是,在这四个方法的参数中,传递的是 UITouch 类型的一个集合
(而不是一个 UITouch),这对应了两根及以上手指触摸同一个视图的情况。
当用户点击屏幕时,事件传递的顺序:
1. 用户在点击屏幕
;
2. 系统将点击事件加入到 UIApplication
管理的消息队列中;
3. UIApplication
会从消息队列中取出该事件传递给 UIWindow
对象;
4. 在 UIWindow
中调用方法 hitTest:withEvent:
,在 hitTest:withEvent:
方法中调用 pointInside:withEvent:
来判断当前点击的点是否在 UIWindow
内部;
5. 如若返回 yes
,则倒序遍历其子视图找到最终响应的子 view
;
6. 如果最终返回一个view
,那么即为最终响应 view
并结束事件传递,如果无值返回则将 UIWindow
作为响应者
。
两个核心方法:
// 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;
- 方法
hitTest:withEvent:
用来获取最终响应事件的view
。 - 方法
pointInside:withEvent:
,用来判断点击的位置是否在视图范围内。
事件响应链
事件响应就是将离用户最近的view传递回去,举个例子:
图中浅灰色的箭头是指将 UIView
直接添加到 UIWindow
上情况。
响应链应该是:ViewB
->ViewC
-> ViewA
-> UIViewController 对象
-> UIWindow 对象
-> UIApplication 对象
-> App Delegate
触摸事件首先将会由第一响应者响应,触发其 (target action) 等方法,根据触摸的方式不同(如拖动,双指),具体的方法和过程也不一样。若第一响应者在这个方法中不处理这个事件,则会传递给响应链中的下一个响应者触发该方法处理,若下一个也不处理,则以此类推传递下去。若到最后还没有人响应,则会被丢弃(比如一个误触)。
点击穿透事件
看看这两个Button重合在了一起,我们都知道点击1区域是粉色Button响应了,点击3区域是蓝色区域响应了,那我们点击区域2呢,谁来响应?接下来我们就以点击区域2粉色Button响应来学习一下点击穿透事件。
那么思路是怎么样的呢:
1. 点击蓝色按钮的区域2,粉色按钮响应事件,那肯定要重写蓝色按钮的hitTest
方法
2. 在hitTest方法中,将触摸点的坐标系从蓝色按钮转换到粉色按钮上,即以粉色按钮左上角为原点
3. 坐标系转换后,判断触摸点是否在粉色按钮上,如果是,直接返回粉色按钮(严谨一点的做法是调用粉色按钮的hitTest方法),如果不是,那就调用系统的方法,让系统去处理
我们来写一下代码,既然要重新定义Button的尺寸信息,那我们就自定义一个类继承于UIButton
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface BlueButton : UIButton
@property(nonatomic, weak)UIButton *PinkButton;
@end
NS_ASSUME_NONNULL_END
#import "BlueButton.h"
@implementation BlueButton
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint PinkButtonPoint = [self convertPoint:point toView:_PinkButton];
if ([_PinkButton pointInside:PinkButtonPoint withEvent:event]) {
return _PinkButton;
}
//如果希望严谨一点,可以将上面if语句及里面代码替换成如下代码
//UIView *view = [_redButton hitTest: PinkButtonPoint withEvent: event];
//if (view) return view;
return [super hitTest:point withEvent:event];
}
@end
接着在ViewController里:
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
//@property(nonatomic, strong) UIButton *BlueButton;
@property(nonatomic, strong) UIButton *PinkButton;
@end
#import "ViewController.h"
#import "BlueButton.h"
@interface ViewController ()
@property (nonatomic, strong) BlueButton *BlueButton;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_PinkButton = [UIButton buttonWithType:UIButtonTypeSystem];
_PinkButton.frame = CGRectMake(self.view.frame.size.width / 2 - 50, self.view.frame.size.height / 2 - 50, 100, 100);
[_PinkButton addTarget:self action:@selector(PinkTouch) forControlEvents:UIControlEventTouchUpInside];
_PinkButton.backgroundColor = [UIColor systemPinkColor];
[self.view addSubview:_PinkButton];
self.BlueButton = [BlueButton buttonWithType:UIButtonTypeSystem];
self.BlueButton.frame = CGRectMake(self.view.frame.size.width / 2 , self.view.frame.size.height / 2 , 100, 100);
[self.BlueButton addTarget:self action:@selector(BlueTouch) forControlEvents:UIControlEventTouchUpInside];
self.BlueButton.backgroundColor = [UIColor blueColor];
self.BlueButton.PinkButton = _PinkButton;
[self.view addSubview:self.BlueButton];
}
- (void)BlueTouch {
NSLog(@"触碰蓝色按钮");
}
-(void)PinkTouch {
NSLog(@"触碰粉色按钮");
}
@end
因为我们重写了蓝色按钮的hitTest方法,所以此时我们依次点击区域1,2,3
此时我们的目标就达成了
demo地址点击穿透事件demo
扩大点击区域
和点击穿透事件一样我们来重写一个类继承于UIButton,然后重写其中的方法来达到我们想要的效果
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface BiggerButton : UIButton
@end
#import "BiggerButton.h"
@implementation BiggerButton
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
//当前button大小
CGRect buttonBounds = self.bounds;
//扩大点击区域,想缩小就将-10设为正值
buttonBounds = CGRectInset(buttonBounds, -100, -100);
//若点击的点在新的bounds里,就返回YES
return CGRectContainsPoint(buttonBounds, point);
}
@end
我们可以看到我们重写的方法就是-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
然后我们在ViewController.m中:
#import "ViewController.h"
#import "BiggerButton.h"
@interface ViewController ()
@property(nonatomic, strong) BiggerButton *BigButton;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.BigButton = [BiggerButton buttonWithType:UIButtonTypeSystem];
self.BigButton.frame = CGRectMake(100, 100, 100, 100);
self.BigButton.backgroundColor = [UIColor blueColor];
[self.BigButton addTarget:self action:@selector(BigButtonTouch) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.BigButton];
}
-(void)BigButtonTouch {
NSLog(@"点击了扩大按钮区域");
}
@end
此时我们的蓝色Button的点击区域已经被放大了10倍,所以我们点击它附近的空白区域也会触发点击事件:
但要注意,它只是被放大了,放大之后仍有一个范围超过范围点击不会触发点击事件