事件类型
- 在iOS设备会产生各种各样的事件(UIEvent 实例)比如:触摸屏幕、远程控制等,这些事件发生了就需要有响应者(UIResponder 实例)去响应这些事件,在iOS中事件的定义为
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,
};
-
UIEventTypeTouches
:触摸事件; -
UIEventTypeMotion
:设备晃动,类似于传感器之类的事件; -
UIEventTypeRemoteControl
:远程控制事件; - 本篇着重探讨触摸事件;
触摸事件的产生
- 当用户的手指触摸到iOS手机屏幕时,一个触摸事件就在系统中产生了,然后iOS系统通过
mach port转发
,将触摸事件传递给当前正在运行的进程,然后在当前进程中寻找到第一响应者,进行响应,其主体流程如下: - 当手指触摸到屏幕, 屏幕感受到触摸后, 系统会将触摸交给
IOKit.framework
处理,IOKit.framework在内部会将触摸封装成一个IOHIDEvent对象
,并且通过mach port
递交给SpringBoard.app(系统桌面)
进程; -
SpringBoard进程
收到这个IOHIDEvent对象
,会触发SpringBoard.app进程中的主线程runloop的source1事件回调
,此时SpringBoard.app会根据桌面状态,判断是否有运行在前台的进程,若无app在前台运行, 那么会触发SpringBoard.app进程主线程runloop的source0回调
,事件会在SpringBoard进程内部被消耗,若有app在前台运行,则SpringBoard通过mach port
将IOHIDEvent分发给该app,接下来就是该app内部消耗这个IOHIDEvent对象的过程; - 当前正在运行的app进程接收到这个IOHIDEvent对象,其
主线程的runloop被唤醒
, 并触发source0回调
,在这个source0回调中,IOHIDEvent对象被封装成为一个UIEvent对象
,并且将这个UIEvent对象添加到给UIApplication对象的事件队列
中,当事件出队后,UIApplication开始寻找事件的第一响应者
,此过程被称为Hit-Testing
过程;
UIResponder响应者对象
- 触摸事件产生后,进入当前app进行中会被封装成
UIEvent对象
,那么当前app需要对这个触摸事件作出响应,也是说对此触摸事件作出处理,在iOS中只有继承自UIResponder
的对象,才能接受并响应处理事件,例如UIApplication
,UIWindow
,UIView
均可为响应者对象; -
UIResponder
对象,响应处理触摸事件,iOS系统提供了以下几个接口函数;
- (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;
Hit-Testing过程 -- 事件的传递链
- 我们知道只要继承自
UIResponder
的对象,均可以接受并响应处理事件,我们的app的UI结构层级复杂,这些看得见摸得着的UI控件都是可以响应触摸事件的,但是触摸事件的第一响应者
只有一个,其拥有事件处理的绝对控制优先权
,在复杂的UI层级结构中,寻找事件第一响应者的过程,称之为Hit-Testing过程
- 在
Hit-Testing过程
中主要涉及到两个重要函数:
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
- 第一个函数:当前控件在其内部寻找第一响应者;
- 第二个函数:判断触摸点,是否在当前控件内部;
- 控件寻找第一响应者的逻辑流程如下:
- 首先判断当前控件
是否能接收触摸事件
,控件能接收触摸事件需要满足三个条件:- 控件的userInteractionEnabled = YES,即开启交互;
- 控件的hidden属性为NO,即不隐藏;
- 控件的alpha > 0.01,即透明度 > 0.01;
- 其次判断
触摸点是否在当前控件的内部
; - 最后往前
遍历当前控件内部的所有子控件
,递归
前面的逻辑流程;
- 首先判断当前控件
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3种状态无法响应事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//从后往前遍历子视图数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--)
{
// 获取子视图
UIView *childView = self.subviews[I];
// 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
CGPoint childP = [self convertPoint:point toView:childView];
//询问子视图层级中的最佳响应视图
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView)
{
//如果子视图中有更合适的就返回
return fitView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}
- 下面举一个实际例子来阐述事件的传递链:
Snip20210530_57.png
- 所有颜色的view,都是自定义的view类,并重写了touchs的四个事件响应方法,以及hitTest的方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super hitTest:point withEvent:event];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super pointInside:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
- 当用户点击蓝色区域时,控制台输出结果如下:
Snip20210530_59.png
- 看到
hitTest
函数的调用顺序,可以证明上面的逻辑实现是正确的; - 最终找到
YYBuleView
是第一响应者,所以其会调用touches的函数,作出响应处理; - 当用户点击灰色区域时,控制台输出结果如下:
Snip20210530_60.png
- 最终找到
YYGrayView
是第一响应者; - 在
YYBuleView
的touchesEnded方法中加入以下代码:
UIResponder *next = [self nextResponder];
NSMutableString *prefix = @"".mutableCopy;
while (next != nil) {
NSLog(@"%@%@", prefix, [next class]);
[prefix appendString: @"--"];
next = [next nextResponder];
}
- 点击蓝色区域,控制台输出结果如下:
Snip20210530_62.png
- 可以看到触摸事件的传递链为:
AppDelegate
-->UIApplication
-->UIWindowScene
-->UIWindow
-->UITransitionView
-->UIDropshadowView
-->ViewController
-->UIView
-->YYGrayView
-->YYGreenView
-->YYBuleView
事件的响应链
- 上面说到了点击蓝色区域,事件的传递链为:
AppDelegate
-->UIApplication
-->UIWindowScene
-->UIWindow
-->UITransitionView
-->UIDropshadowView
-->ViewController
-->UIView
-->YYGrayView
-->YYGreenView
-->YYBuleView
是自下而上的; - 那么事件的响应链与传递链完全相反:
YYBuleView
-->YYGreenView
-->YYGrayView
-->UIView
-->ViewController
-->UIDropshadowView
-->UITransitionView
-->UIWindow
-->UIWindowScene
-->UIApplication
-->AppDelegate
,其中YYBuleView
为第一响应者,是自上而下的;
UIGestureRecognizer手势识别器
- 在iOS中常见的手势识别器有以下六种:
-
UITapGestureRecognizer
:点击 -
UIPinchGestureRecognizer
:缩放 -
UIPanGestureRecognizer
:平移或拖动 -
UISwipeGestureRecognizer
:滑动 -
UIRotationGestureRecognizer
:旋转(手指沿相反方向移动) -
UILongPressGestureRecognizer
:长按
-
- 这6六种手势识别器又可以分为两大类:
- 离散型手势:UITapGestureRecognizer与UISwipeGestureRecognizer
- 持续性手势:其他四种;
- 手势识别器通常是添加到目标控件上,在识别手势的过程总会存在以下几种状态:
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible,
UIGestureRecognizerStateBegan,
UIGestureRecognizerStateChanged,
UIGestureRecognizerStateEnded,
UIGestureRecognizerStateCancelled,
UIGestureRecognizerStateFailed,
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
UIGestureRecognizerStatePossible
};
-
UIGestureRecognizerStatePossible
:触摸事件已经发生,但手势尚未被识别,这是手势的默认初始状态; -
UIGestureRecognizerStateBegan
:手势开始 -
UIGestureRecognizerStateChanged
:手势变化 -
UIGestureRecognizerStateEnded
:手势结束 -
UIGestureRecognizerStateCancelled
:手势取消 -
UIGestureRecognizerStateFailed
:手势失败 -
UIGestureRecognizerStateRecognized
:手势已经被识别 - 我们知道继承自UIResponder的对象,都具有响应处理事件的能力,主要是通过下面四个接口方法实现的:
- (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;
- 手势识别器也同样拥有这四个接口方法,来响应处理事件,但这四个接口方法是在
#import <UIKit/UIGestureRecognizerSubclass.h>
文件中声明的;
Snip20210602_65.png
- 测试代码如下:
- 自定义离散型手势
YYTapGestureRecognizer
#import "YYTapGestureRecognizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
@implementation YYTapGestureRecognizer
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesCancelled:touches withEvent:event];
}
@end
- 自定义view
YYYellowView
#import "YYYellowView.h"
@implementation YYYellowView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super hitTest:point withEvent:event];
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super pointInside:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
- 在控制器的view上一个YYYellowView,在YYYellowView上添加一个YYTapGestureRecognizer手势;
#import "YYThirdVC.h"
#import "YYYellowView.h"
#import "YYTapGestureRecognizer.h"
@interface YYThirdVC ()
@end
@implementation YYThirdVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
YYTapGestureRecognizer *tap = [[YYTapGestureRecognizer alloc]initWithTarget:self action:@selector(tap:)];
YYYellowView *yView = [[YYYellowView alloc]initWithFrame:CGRectMake(20, 100, 150, 100)];
yView.backgroundColor = [UIColor yellowColor];
[yView addGestureRecognizer:tap];
[self.view addSubview:yView];
}
- (void)tap:(UITapGestureRecognizer *)sender{
NSLog(@"%s",__func__);
}
@end
- 在
YYYellowView
的- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
函数中加入断点,点击YYYellowView时,调试结果如下:
Snip20210603_68.png
- 可以看到事件对象
UIEvent
的属性数组allTouchesMutable
中存储了所有触摸对象UITouch
- 触摸对象UITouch所在的
UIWindow
与所在的view
都可以看到; - 触摸对象UITouch中包含的
所有手势识别器
都存储在gestureRecogizers
数组中,其中包含了三个手势识别器,YYTapGestureRecognizer
是自定义的,后面两个是系统的; - 控制台的打印结果如下:
Snip20210603_69.png
- 可以看到在执行HitTest时,找到事件第一响应者之后,
手势识别器优先响应了事件
,然后第一响应者再响应事件
,当手势识别器结束响应,调用其回调函数后
,第一响应者取消了对此次事件的响应
- 也就是说
手势识别响应事件的优先级高于事件的第一响应者
- 上面是针对离散型手势,现在自定义一个持续性手势
YYPanGestureRecongnizer
:
#import "YYPanGestureRecongnizer.h"
#import <UIKit/UIGestureRecognizerSubclass.h>
@implementation YYPanGestureRecongnizer
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesCancelled:touches withEvent:event];
}
@end
- 调试结果如下:
Snip20210603_70.png
- 执行流程:
- HitTest寻找事件的第一响应者;
- 手势识别器优先响应事件,然后第一响应者再响应事件;
- 当手势识别器第一次执行自己的回调函数时,第一响应者取消了对事件的响应;
- 接下来只有手势识别器响应事件;
手势识别器的三个属性
-
cancelsTouchesInView
:即当手势识别器识别成功,执行自己的回调函数之后,事件的第一响应者会取消对事件的响应,默认为YES,若设置为NO,那么当手势识别器识别成功后,手势识别器和第一响应者可同时响应事件
;
#import "YYThirdVC.h"
#import "YYYellowView.h"
#import "YYTapGestureRecognizer.h"
#import "YYPanGestureRecongnizer.h"
@interface YYThirdVC ()
@end
@implementation YYThirdVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
YYPanGestureRecongnizer *pan = [[YYPanGestureRecongnizer alloc]initWithTarget:self action:@selector(pan:)];
pan.cancelsTouchesInView = NO;
[self.view addGestureRecognizer:pan];
YYYellowView *yView = [[YYYellowView alloc]initWithFrame:CGRectMake(20, 100, 150, 100)];
yView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:yView];
}
- (void)pan:(UIPanGestureRecognizer *)sender{
NSLog(@"%s",__func__);
}
@end
- 调试结果如下:
Snip20210603_71.png
-
delaysTouchesBegan
:其值为YES时,表示在手势识别器识别期间,不会将事件传递给第一响应者,手势识别成功后,更不会有第一响应者响应事件,默认为NO;
#import "YYThirdVC.h"
#import "YYYellowView.h"
#import "YYTapGestureRecognizer.h"
#import "YYPanGestureRecongnizer.h"
@interface YYThirdVC ()
@end
@implementation YYThirdVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
YYPanGestureRecongnizer *pan = [[YYPanGestureRecongnizer alloc]initWithTarget:self action:@selector(pan:)];
pan.delaysTouchesBegan = YES;
[self.view addGestureRecognizer:pan];
YYYellowView *yView = [[YYYellowView alloc]initWithFrame:CGRectMake(20, 100, 150, 100)];
yView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:yView];
}
- (void)pan:(UIPanGestureRecognizer *)sender{
NSLog(@"%s",__func__);
}
@end
- 调试结果如下:
Snip20210603_72.png
-
delaysTouchesEnded
:默认为YES;当手势识别失败时,若此时触摸已经结束,会延迟一小段时间(0.15s)再调用第一响应者的 touchesEnded:withEvent:,若设置成NO,则在手势识别失败时会立即通知Application发送状态为end的touch事件给Hit-Test View以调用 touchesEnded:withEvent:函数来结束事件的响应;
UIControl
- UIControl继承自UIResponser,所以其可以响应处理事件,在iOS中UIButton、UISegmentedControl、UISwitch等控件都是UIControl的子类,当UIControl跟踪到触摸事件时,会向其 添加的target发送事件以执行action回调;
- UIControl响应事件的也有属于自身的一套函数方法,如下所示:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;
- 由于UIControl继承自UIResponser,所以也会响应UIResponser所提供的响应事件的个函数方法:
- (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;
- 事实上,UIControl的 Tracking 系列方法是在UIResponser 的 touch 系列方法内部调用的,比如 beginTrackingWithTouch 是在 touchesBegan 方法内部调用的;
- 代码测试:自定义
YYButton
#import "YYButton.h"
@implementation YYButton
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
[super touchesCancelled:touches withEvent:event];
}
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super beginTrackingWithTouch:touch withEvent:event];
}
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
return [super continueTrackingWithTouch:touch withEvent:event];
}
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event{
NSLog(@"%s",__func__);
}
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
- 在控制器上添加自定义的
YYButton
代码如下:
#import "YYThirdVC.h"
#import "YYYellowView.h"
#import "YYTapGestureRecognizer.h"
#import "YYControlVC.h"
@interface YYThirdVC ()
@end
@implementation YYThirdVC
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
YYTapGestureRecognizer *tap = [[YYTapGestureRecognizer alloc]initWithTarget:self action:@selector(tap:)];
YYButton *b = [YYButton new];
b.frame = CGRectMake(20, 400, 100, 50);
b.backgroundColor = [UIColor redColor];
[b addTarget:self action:@selector(click:) forControlEvents:(UIControlEventTouchUpInside)];
[b addGestureRecognizer:tap];
[self.view addSubview:b];
}
- (void)click:(UIButton *)sender{
YYControlVC *vc = [[YYControlVC alloc]init];
[self.navigationController pushViewController:vc animated:YES];
}
- (void)tap:(UITapGestureRecognizer *)sender{
NSLog(@"%s",__func__);
}
@end
- 将手势识别器加在按钮上,控制台调试结果如下:
Snip20210603_75.png
- 手势识别器与按钮的target-action处于同一个层级,按钮不会阻断手势识别器对事件的响应,所以手势识别器能成功识别并执行自己的回调函数;
- 又由于按钮作为事件的第一响应者,在手势识别器成功识别执行回调后,会取消对事件的响应;
- 将手势识别器添加到父控件上(控制器的view),代码如下:
#import "ViewController.h"
#import "YYTapGestureRecognizer.h"
#import "YYButton.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
YYTapGestureRecognizer *tap = [[YYTapGestureRecognizer alloc]initWithTarget:self action:@selector(tap:)];
[self.view addGestureRecognizer:tap];
YYButton *b = [YYButton new];
b.frame = CGRectMake(20, 100, 100, 50);
b.backgroundColor = [UIColor redColor];
[b addTarget:self action:@selector(click:) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:b];
}
- (void)click:(UIButton *)sender{
NSLog(@"%s",__func__);
}
- (void)tap:(YYTapGestureRecognizer *)sender{
NSLog(@"%s",__func__);
}
@end
- 点击按钮时,调试结果如下:
image.png
- 手势识别器率先响应事件;
- 其次按钮在响应事件;
- 最后按钮会阻断手势识别器对事件的响应,最终由按钮的target-action处理事件;
最后得出结论:
- 当手势识别器加在父控件上,子控件按钮会阻断手势识别器对事件的响应,最终由按钮的target-action处理事件;
- 当识别器加在按钮上与按钮的target-action处于同一个层级按钮不会阻断手势识别器对事件的响应,所以手势识别器能成功识别并执行自己的回调函数;
- 总结:
- 事件处理的优先级:UIGestureRecognizer > UIResponser = UIControl
- UIControl继承自UIResponser,但其内部又提供了一套响应事件的函数;
实战应用
案例一:子控件超出父控件区域
- 问题描述:子控件超出父控件的显示区域,当点击超出区域时,子控件的点击事件会失效,如下图所示:
Snip20210530_63.png
- 页面层级结构:UIViewController --> view --> 自定义的底部灰色view --> 绿色圆形按钮
- 当点击绿色圆形按钮的上半部分时,按钮的点击事件失效;
- 原因分析:
- 根据事件传递与响应的原理,我们知道当点击绿色圆形按钮的下半部分时的事件传递链为:
AppDelegate
-->UIApplication
-->UIWindowScene
-->UIWindow
-->UITransitionView
-->UIDropshadowView
-->ViewController
-->UIView
-->自定义的底部灰色view
-->绿色圆形按钮
,按钮能正常响应事件; - 当点击绿色圆形按钮的上半部分时,在Hit-Testing过程中,来到控制器的UIView时,此时控制器的UIView只有一个子控件 -->
自定义的底部灰色view
,触摸点不在底部灰色view区域内,所以控制器的UIView成为事件的第一响应者
,事件根本传递不到绿色圆形按钮,所以其点击事件失效;
- 根据事件传递与响应的原理,我们知道当点击绿色圆形按钮的下半部分时的事件传递链为:
- 解决方案:
- 在
Hit-Testing
过程中,当事件传递到自定义的底部灰色view时,自定义的底部灰色view的hitTest:withEvent:
被调用,但是pointInside:withEvent:
会返回NO,如此一来 hitTest:withEvent: 返回了nil。既然如此,可以重写自定义的底部灰色view的 pointInside:withEvent: ,判断当前触摸坐标是否在子视图CircleButton的坐标范围内,若在,则返回YES,反之返回NO。这样一来点击自定义的底部灰色view的上半部分,事件最终会传递到CircleButton,CircleButton能够响应事件,最终事件就由CircleButton响应了,代码实现如下:
- 在
#import "YYBottomView.h"
@interface YYBottomView ()
@property(nonatomic,strong)UIButton *circle;
@end
@implementation YYBottomView
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
[self createUI];
}
return self;
}
- (void)createUI{
[self addSubview:self.circle];
self.circle.frame = CGRectMake(self.frame.size.width/2 - 40/2, -20, 40, 40);
}
- (void)click:(UIButton *)sender{
NSLog(@"%s",__func__);
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
//将触摸点坐标转换到在CircleButton上的坐标
CGPoint pointTemp = [self convertPoint:point toView:self.circle];
//若触摸点在CricleButton上则返回YES
if ([self.circle pointInside:pointTemp withEvent:event]) {
return YES;
}
//否则返回默认的操作
return [super pointInside:point withEvent:event];
}
#pragma mark lazy
- (UIButton *)circle{
if (!_circle) {
_circle = [UIButton buttonWithType:UIButtonTypeCustom];
[_circle addTarget:self action:@selector(click:) forControlEvents:(UIControlEventTouchUpInside)];
_circle.backgroundColor = [UIColor greenColor];
_circle.layer.cornerRadius = 20;
}
return _circle;
}
@end