这个应该是我们最经常接触的一个part啦~ 最近周末有点儿沉迷泰国耽美剧,但周更还是要有的~
iOS中的UIEvent主要有三种:触摸事件、加速计事件、远程控制事件,加速计就是计步的原理,远程控制就是类似我们用耳机可以调节手机的音量快进之类的。但这里还是以触摸事件为主。
1. 事件传递
当我们点击屏幕最先拿到事件的是谁呢?其实是UIApplication。所以其实事件传递是自上而下的,从UIApplication开始寻找谁是最适合来响应这个事件的view
- 例如当我们点了VC里面的一个button,这个点击事件的传递是:
- 将该事件加入到一个由UIApplication管理的FIFO的事件队列中。(事件的处理逻辑应该是先发生先响应)
- 每次UIApplication会从事件队列中找到最前面的事件,并将事件给应用程序的主窗口UIWindow,然后window会遍历它的子view依次向下传递
- 找到最适合的view后,就会调用view的触摸方法啦
下面的一个问题就是怎么找到最合适的view呢?
其实就是通过下面的这个方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
父view会依次调用子view的hitTest方法,如果都返回了nil,并且父view可以响应事件,父view就返回自己;如果子view的hitTest方法返回不为空,则返回子view的返回值。
这里提到了如果父view可以响应才会去找有木有更合适的子view来处理,那么"可以响应"的定义是啥呢?
isUserInteractionEnabled设置为NO、隐藏、alpha小于等于0.01的视图的view都不可以响应事件哦
所以其实hitTest的代码类似酱紫:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//系统默认会忽略isUserInteractionEnabled设置为NO、隐藏、alpha小于等于0.01的视图
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;
}
看这里循环遍历子view的时候用的是reverseObjectEnumerator
,也就是说后加入的view会先被遍历,如果它已经是一个合适的view了(也就是hitTest返回了一个不为nil的值),那么它父view就会停止遍历啦。这也是为了让视觉上在上层的view先处理事件。
那么这里有个小问题我们来看一下,就是如果我们add了很多子view,然后把第一个add的在最底层的view bringToFront,那么它会被最先遍历还是最后遍历到呢?
#import "TouchView.h"
@implementation TouchView
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return [super pointInside:point withEvent:event];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
NSLog(@"hitTest called : %@", self);
return [super hitTest:point withEvent:event];
}
@end
=================
@implementation TouchViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self setupSubviews];
}
- (void)setupSubviews {
TouchView *view1 = [[TouchView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
view1.backgroundColor = [UIColor yellowColor];
TouchView *view2 = [[TouchView alloc] initWithFrame:CGRectMake(60, 60, 200, 200)];
view2.backgroundColor = [UIColor greenColor];
TouchView *view3 = [[TouchView alloc] initWithFrame:CGRectMake(70, 70, 200, 200)];
view3.backgroundColor = [UIColor blueColor];
[self.view addSubview:view1];
[self.view addSubview:view2];
[self.view addSubview:view3];
[self.view bringSubviewToFront:view1];
}
@end
这个时候当我们点击公共区域,打印的是:
2020-03-15 12:37:50.325556+0800 Example1[3274:703263] hitTest called : >
2020-03-15 12:37:50.326718+0800 Example1[3274:703263] hitTest called : >
也就是说因为我们bringToFront的时候其实改了subview的数组顺序,所以虽然view1是第一个被add的,但是它被bringSubviewToFront以后处于了subviews数组的最后一个,所以当我们点了一下以后这三个view的父view只问了view1是不是合适的view,然后view1说是,就没有再问2和3了。
有木有发现一次触摸调用了两次hitTest?
对于一次tap,hitTest会被调用两次。这个问题在Apple Mailing List Re: -hitTest:withEvent: called twice?里面有描述:
Yes, it’s normal. The system may tweak the point being hit tested between the calls. Since hitTest should be a pure function with no side-effects, this should be fine.
当我们在UIView中重写hitTest
也可以发现:这两次调用的timestamp都几乎是相同的,并且两次hitTest
先被调用之后才调用了touchesBegan
和touchesEnded
。
还有就是如果我们按下手指然后移动,只会调用一次hitTest,相当于我们,move手指算作一个event,不会多次调用hitTest哦。
另外需要注意,这个就是默认的hitTest的逻辑,有的时候我们覆写了hitTest
,然后再某些情况下会return super的hitTest就是这个逻辑,而不是调用了superView的hitTest。
在事件传递中一定要注意区分superView和super哈~
例如参考文章里面tabbar超出点击区域的例子,其实这个例子和我之前说过的如果在cell里面加一个气泡,但是气泡超出了cell的区域有一点像。那个例子还有一个解决方式就是让cell有气泡的时候如果点击的是cell外气泡内区域,则hitTest返回不为nil,但是那样不太好毕竟覆写了方法。
因为中间部分超出了tabbar的区域,当用户点击那个button的时候,tabbar的父view会问tabbar要一个最合适的view(调用tabbar的hitTest),但是由于point根本不in tabbar,所以tabbar会返回nil,事件也就不会交给它啦。
如果我们想让中间的部分可以被点击到,则可以改写tabbar的hitTest
:
//重写hitTest方法,去监听中间按钮的点击,目的是为了让凸出的部分点击也有反应
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//判断当前手指是否点击到中间按钮上,如果是,则响应按钮点击,其他则系统处理
//首先判断当前View是否被隐藏了,隐藏了就不需要处理了
if (self.isHidden == NO) {
//将当前tabbar的触摸点转换坐标系,转换到中间按钮的身上,生成一个新的点
CGPoint newP = [self convertPoint:point toView:self.centerBtn];
//判断如果这个新的点是在中间按钮身上,那么处理点击事件最合适的view就是中间按钮
if ( [self.centerBtn pointInside:newP withEvent:event]) {
return self.centerBtn;
}
}
return [super hitTest:point withEvent:event];
}
当然还有一个办法,就是你重写tabbar的pointInside也是可以的~~
所以其实找到最合适的view主要就是靠两个方法:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
2. 事件响应
为什么UIView可以响应事件呢?其实是因为他们继承自UIResponder,而且其实AppDelegate也是继承自UIResponder,毕竟UIApplication也是可以响应事件的。
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIView : UIResponder
那么UIResponder里面有什么呢?
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIResponder : NSObject
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO
- (BOOL)becomeFirstResponder;
@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES
- (BOOL)resignFirstResponder;
@property(nonatomic, readonly) BOOL isFirstResponder;
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet *)touches API_AVAILABLE(ios(9.1));
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event API_AVAILABLE(ios(9.0));
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event API_AVAILABLE(ios(3.0));
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event API_AVAILABLE(ios(4.0));
- (BOOL)canPerformAction:(SEL)action withSender:(nullable id)sender API_AVAILABLE(ios(3.0));
// Allows an action to be forwarded to another target. By default checks -canPerformAction:withSender: to either return self, or go up the responder chain.
- (nullable id)targetForAction:(SEL)action withSender:(nullable id)sender API_AVAILABLE(ios(7.0));
// Overrides for menu building and validation
- (void)buildMenuWithBuilder:(id)builder;
- (void)validateCommand:(UICommand *)command;
@property(nullable, nonatomic,readonly) NSUndoManager *undoManager API_AVAILABLE(ios(3.0));
// Productivity editing interaction support for undo/redo/cut/copy/paste gestures
@property (nonatomic, readonly) UIEditingInteractionConfiguration editingInteractionConfiguration API_AVAILABLE(ios(13.0));
@end
这里的touch、press、motion的四个方法其实都是类似的:
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
注意哦,官方特意提示,当覆写的时候一定要处理cancel事件,否则可能会有错误的behavior或者crash
尝试覆写一下touchBegan看一下当我们不同的操作会打印怎样的event和touch吧~
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan touches : %@", touches);
NSLog(@"touchesBegan event : %@", event);
[super touchesBegan:touches withEvent:event];
}
先用一个手指轻轻点击一下,输出是酱紫的:
2020-03-15 13:25:04.296468+0800 Example1[3356:732126] touchesBegan touches : {(
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {144, 168} previous location in window: {144, 168} location in view: {94, 118} previous location in view: {94, 118}
)}
2020-03-15 13:25:04.297363+0800 Example1[3356:732126] touchesBegan event : timestamp: 90760 touches: {(
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {144, 168} previous location in window: {144, 168} location in view: {94, 118} previous location in view: {94, 118}
)}
可以看到touch的tapCount是1,event是UITouchesEvent类型,所以自然想到如果我们连续双击打印出的是什么呢?
2020-03-15 13:28:22.756868+0800 Example1[3359:733010] touchesBegan touches : {(
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {152, 186.5} previous location in window: {152, 186.5} location in view: {102, 136.5} previous location in view: {102, 136.5}
)}
2020-03-15 13:28:22.757916+0800 Example1[3359:733010] touchesBegan event : timestamp: 90958.5 touches: {(
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {152, 186.5} previous location in window: {152, 186.5} location in view: {102, 136.5} previous location in view: {102, 136.5}
)}
2020-03-15 13:28:22.896354+0800 Example1[3359:733010] touchesBegan touches : {(
phase: Began tap count: 2 force: 0.000 window: ; layer = > view: > location in window: {158.5, 182.5} previous location in window: {158.5, 182.5} location in view: {108.5, 132.5} previous location in view: {108.5, 132.5}
)}
2020-03-15 13:28:22.897359+0800 Example1[3359:733010] touchesBegan event : timestamp: 90958.6 touches: {(
phase: Began tap count: 2 force: 0.000 window: ; layer = > view: > location in window: {158.5, 182.5} previous location in window: {158.5, 182.5} location in view: {108.5, 132.5} previous location in view: {108.5, 132.5}
)}
当用一个手指双击的时候,会调用两次touchBegan哦,不是一次,这两次的区别可以看到第一次的tapCount是1,第二次就是2啦
如果你两次tap之间隔了很久,没有达到双击的要求,第二次调用touchBegan的时候的touch的tapCount就会重新为1。其实tapCount就是当前这次touch是一个连续点击的第几次,注意哦这个是连击才会重复调用touchBegan,如果是一次点击移动手指就会直接调用touchMove啦。
然后会想到的是如果两个手指同时点击会触发什么呢?我第一次这么做的时候打印出来的和只轻轻点一次木有区别,然后就想起来了忘记让view支持多点touch了~
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.multipleTouchEnabled = YES;
}
return self;
}
然后用两个手指同时点击就只会触发一次touchBegan啦,并且里面的touches数组的元素有两个:
2020-03-15 13:55:30.139445+0800 Example1[3373:739519] touchesBegan touches : {(
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {166, 97} previous location in window: {166, 97} location in view: {116, 47} previous location in view: {116, 47},
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {129.5, 206.5} previous location in window: {129.5, 206.5} location in view: {79.5, 156.5} previous location in view: {79.5, 156.5}
)}
2020-03-15 13:55:30.140728+0800 Example1[3373:739519] touchesBegan event : timestamp: 92581.7 touches: {(
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {166, 97} previous location in window: {166, 97} location in view: {116, 47} previous location in view: {116, 47},
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {129.5, 206.5} previous location in window: {129.5, 206.5} location in view: {79.5, 156.5} previous location in view: {79.5, 156.5}
)}
所以UITouch其实就是一个手指或者说是一个触摸点的数据,感觉硬件还是非常厉害的,当一个手指移动的时候,它要识别出这个是一个触摸点的连续移动而不是两个不次的触摸。
这里来看一下UITouch和UIEvent是啥~
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property(nonatomic,readonly) UITouchPhase phase;
@property(nonatomic,readonly) NSUInteger tapCount; // touch down within a certain point within a certain amount of time
@property(nonatomic,readonly) UITouchType type API_AVAILABLE(ios(9.0));
// majorRadius and majorRadiusTolerance are in points
// The majorRadius will be accurate +/- the majorRadiusTolerance
@property(nonatomic,readonly) CGFloat majorRadius API_AVAILABLE(ios(8.0));
@property(nonatomic,readonly) CGFloat majorRadiusTolerance API_AVAILABLE(ios(8.0));
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
@property(nullable,nonatomic,readonly,strong) UIView *view;
@property(nullable,nonatomic,readonly,copy) NSArray *gestureRecognizers API_AVAILABLE(ios(3.2));
===============
UIKIT_EXTERN API_AVAILABLE(ios(2.0)) @interface UIEvent : NSObject
@property(nonatomic,readonly) UIEventType type API_AVAILABLE(ios(3.0));
@property(nonatomic,readonly) UIEventSubtype subtype API_AVAILABLE(ios(3.0));
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property(nonatomic, readonly, nullable) NSSet *allTouches;
- (nullable NSSet *)touchesForWindow:(UIWindow *)window;
- (nullable NSSet *)touchesForView:(UIView *)view;
- (nullable NSSet *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture API_AVAILABLE(ios(3.2));
UITouch保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置,当手指离开屏幕时,系统会销毁相应的UITouch对象。
这个可以通过打印move和began里面的touch来看:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan touches : %@", touches);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesMoved touches : %@", touches);
[super touchesMoved:touches withEvent:event];
}
输出:
2020-03-15 16:47:48.721936+0800 Example1[3886:828812] touchesBegan touches : {(
phase: Began tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {108, 128} previous location in window: {108, 128} location in view: {58, 78} previous location in view: {58, 78}
)}
2020-03-15 16:47:48.729682+0800 Example1[3886:828812] touchesMoved touches : {(
phase: Moved tap count: 1 force: 0.000 window: ; layer = > view: > location in window: {182, 200} previous location in window: {108, 128} location in view: {132, 150} previous location in view: {58, 78}
)}
可以看出来传递的touch是一个对象~~
注意event里面的allTouches可不是在你move的时候把你所有的touch都记录下来,我试了一下如果单点触摸只有一个touch哈。
另外event的类型有下面几种:
typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses API_AVAILABLE(ios(9.0)),
};
typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,
// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,
// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};
UIEvent包含一个或多个的UITouch,分为三类:UIEventTypeTouches触摸事件、UIEventTypeMotion运动事件(例如手机晃动)、UIEventTypeRemoteControl远程控制事件通过其他远程设备触发(例如耳机控制按钮)。这里就呼应了UIResponder的三套方法了。
注意能够响应这三种事件的仍旧要继承UIResponder吼,这个对触摸还是加速度是没区别的。
3. 响应链
在适合处理事件的view响应了事件以后,它默认会把事件再传递给它的父view,层层上传,直到UIApplication或者哪一个view截断了传递链。
所有的视图都是按照一定的结构组织起来的,即树状层次结构,每个view都有自己的superView,包括controller的topmost view(controller的self.view)。
当一个view被add到superView上的时候,他的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。所以响应链就是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过UIResponder的属性nextResponder串连起来的。如下图:
当我给TouchView以及ViewController都加了下面的:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan self : %@", self);
[super touchesBegan:touches withEvent:event];
}
然后再点击一下屏幕会有:
2020-03-15 17:02:33.387567+0800 Example1[3898:834747] touchesBegan self : >
2020-03-15 17:02:33.387893+0800 Example1[3898:834747] touchesBegan self :
也就是说被点击的view以及它所在的VC都响应了touchBegan,这也就是时间的响应。
事件在找最合适的响应者的时候是从上往下层层下发的,但是找到以后的响应是从下往上,层层传递的。
层层上传的其实是写在touchBegan的默认实现里面的,也就是[super touchesBegan:touches withEvent:event]
,所以如果我重写TouchView的touchBegan
让它不要调用super方法会怎样呢?
(注意这里是super是父类啊,不是父view吼,不要看混了)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan self : %@", self);
// [super touchesBegan:touches withEvent:event];
}
再次触摸屏幕就只有:
2020-03-15 17:07:15.872390+0800 Example1[3901:836243] touchesBegan self : >
也就是事件并不会被TouchView再传递给VC了。
view的nextResponder属性记录着它的下一个响应者,就和链表是一样的,所以其实通过不断上询view的responder可以找到最近的VC,也就是view所在的VC:
- (UIViewController *)parentController
{
UIResponder *responder = [self nextResponder];
while (responder) {
if ([responder isKindOfClass:[UIViewController class]]) {
return (UIViewController *)responder;
}
responder = [responder nextResponder];
}
return nil;
}
4. Runloop与事件派发
这部分很推荐这篇:https://www.jianshu.com/p/b6d669fdae6f
- 当一个触摸事件发生的时候是怎么派发给UIApplication的呢?
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。
这个SpringBoard其实是一个标准的应用程序,这个应用程序用来管理IOS的主屏幕,除此之外像启动WindowSever(窗口服务器),bootstrapping(引导应用程序),以及在启动时候系统的一些初始化设置都是由这个特定的应用程序负责的。它是我们IOS程序中,事件的第一个接受者。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event。
随后用 mach port 转发给需要的App进程,随后苹果注册的那个 Source1就会触发回调,回调一个__IOHIDEventSystemClientQueueCallback()的API,这个API会相应触发Source0来调用__UIApplicationHandleEventQueue()
(这里大概是因为SpringBoard和我们的app是跨程序的,所以用mach port的方式传递消息)
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
※ 手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
(这个部分到下面会试一下)
※ 界面刷新
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
5. Gesture
如果给VC加一个左滑手势,并且打印touch的事件,可以看到:
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeGuestureDidTriggered)];
swipe.direction = UISwipeGestureRecognizerDirectionLeft;
[self.view addGestureRecognizer:swipe];
- (void)swipeGuestureDidTriggered {
NSLog(@"swipeGuestureDidTriggered");
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesBegan self : %@", self);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesMoved self : %@", self);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"touchesCancelled self : %@", self);
[super touchesCancelled:touches withEvent:event];
}
当我左滑一下,打印的是酱紫的:
2020-03-15 18:38:00.878090+0800 Example1[3997:870923] touchesBegan self :
2020-03-15 18:38:01.019204+0800 Example1[3997:870923] touchesMoved self :
2020-03-15 18:38:01.027166+0800 Example1[3997:870923] touchesMoved self :
2020-03-15 18:38:01.035809+0800 Example1[3997:870923] touchesMoved self :
2020-03-15 18:38:01.052678+0800 Example1[3997:870923] touchesMoved self :
2020-03-15 18:38:01.069183+0800 Example1[3997:870923] touchesMoved self :
2020-03-15 18:38:01.085810+0800 Example1[3997:870923] touchesMoved self :
2020-03-15 18:38:01.102739+0800 Example1[3997:870923] touchesMoved self :
2020-03-15 18:38:01.119157+0800 Example1[3997:870923] touchesMoved self :
2020-03-15 18:38:01.144741+0800 Example1[3997:870923] swipeGuestureDidTriggered
2020-03-15 18:38:01.145567+0800 Example1[3997:870923] touchesCancelled self :
然后无论我怎么划,上滑下滑左滑右滑只要已经被是被成了gesture,就不会再调用touch的几个事件了,也就证明了上面说的,当gesture被识别出来以后就会cancel掉touch事件。
如果我给VC和TouchView都加了左滑手势,那么打印出来是酱紫的:
2020-03-15 18:44:11.053644+0800 Example1[4000:873173] touchesBegan self :
2020-03-15 18:44:11.146682+0800 Example1[4000:873173] touchesMoved self :
2020-03-15 18:44:11.154195+0800 Example1[4000:873173] touchesMoved self :
...
2020-03-15 18:44:11.232947+0800 Example1[4000:873173] swipeGuestureDidTriggered: ; layer = >
2020-03-15 18:44:11.233885+0800 Example1[4000:873173] touchesCancelled self :
也就是说当子view和父view都有手势的时候,只有子view的手势会响应识别,这个听起来就非常对,毕竟子view在上面啊
但这个是为啥呢?不是说响应链会把事件向上传递么?我们也看到了的确VC的touch事件都被回调了,那么为啥VC并没有识别手势呢
这个原因其实很二,因为默认shouldRecognizeSimultaneouslyWithGestureRecognizer
返回时NO,也就是默认一个操作只会被一个手势识别
如果想让父view和子view都识别这个手势,那么需要两个gesture都加delegate,并且delegate的shouldRecognizeSimultaneouslyWithGestureRecognizer
方法都返回YES。注意只有一个gesture加delegate是不够的,如果另一个是NO仍旧不会被共同触发
※ 手势是如何被识别的
强烈推荐这篇:https://www.jianshu.com/p/77139b374313
其实手势识别就是在那几个touch方法被调用的时候去尝试识别这次的event是不是满足手势的要求,如果满足就会cancel touch的方法并开启手势的began,如果不满足就是fail的识别状态。
所以如果你实现了gesture的delegate你可以打印识别的过程,手势的状态会有下面几种:
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
UIGestureRecognizerStateBegan, // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateChanged, // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateEnded, // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateCancelled, // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateFailed, // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
// Discrete Gestures – gesture recognizers that recognize a discrete event but do not report changes (for example, a tap) do not transition through the Began and Changed states and can not fail or be cancelled
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
};
下面就是人家的实验了,小姐姐偷个懒
图中baseView 有两个subView,分别是testView和testBtn。我们在baseView和testView都重载touchsBegan:withEvent
、touchsEnded:withEvent
、
touchsMoved:withEvent
、touchsCancelled:withEvent
方法,并且在baseView上添加单击手势,action名为tapAction
,给testBtn绑定action名为testBtnClicked
。
//baseView
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
[self.view addGestureRecognizer:tap];
...
[_testBtn addTarget:self action:@selector(testBtnClicked) forControlEvents:UIControlEventTouchUpInside];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Began");
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Moved");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Ended");
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> base view touchs Cancelled");
}
- (void)tapAction {
NSLog(@"=========> single Tapped");
}
- (void)testBtnClicked {
NSLog(@"=========> click testbtn");
}
//test view
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Began");
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Moved");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Ended");
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"=========> test view touchs Cancelled");
}
情景A :单击baseView,输出结果为:
=========> base view touchs Began
=========> single Tapped
=========> base view touchs Cancelled
情景B :单击testView,输出结果为:
=========> test view touchs Began
=========> single Tapped
=========> test view touchs Cancelled
情景C :单击testBtn, 输出结果为:
=========> click testbtn
情景D :按住testView,过5秒后或更久释放,输出结果为:
=========> test view touchs Began
=========> test view touchs Ended
其实除了情景C其他都很好理解,因为手势有更高的优先级来识别一个触摸事件,所以当手势识别以后,touch就会被cancel。
Gesture Recognizers Get the First Opportunity to Recognize a Touch. A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. During the delay, if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.
触摸事件首先传递到手势上,如果手势识别成功,就会取消事件的继续传递,否则,事件还是会被响应链处理。具体地,系统维持了与响应链关联的所有手势,事件首先发给这些手势,然后再发给响应链。
但是情景C为啥不是baseView响应手势先呢?这就涉及到一个响应级别的问题了,iOS开发文档里说到
In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer. This applies only to gesture recognition that overlaps the default action for a control, which includes:
※ A single finger single tap on a UIButton, UISwitch, UISegmentedControl, UIStepper,and UIPageControl.
※ A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
※ A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.
也就是如果父view加了和子UIControl类的默认action一致的手势,只有子view的action会被触发,父view的gesture就不会了。
如果是button既有点击action又有tap gesture呢?
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(100, 500, 100, 100);
[button setTitle:@"click here" forState:UIControlStateNormal];
[button setBackgroundColor:[UIColor greenColor]];
[button addTarget:self action:@selector(btnClicked) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGuestureDidTriggered:)];
tap.delegate = self;
[button addGestureRecognizer:tap];
- (void)tapGuestureDidTriggered:(UIGestureRecognizer *)gesture {
NSLog(@"tapGuestureDidTriggered: %@", gesture.view);
}
然后点击一下button以后会打印:
2020-03-15 21:59:33.292353+0800 Example1[4180:934813] tapGuestureDidTriggered: ; layer = >
其实也很好理解,因为gesture的优先级最高,所以button只会触发自己的tap手势,不会触发action啦。
在情景D中,由于长按住testView不释放,tap手势就会识别失败,然后就可以继续正常传递给testView处理。
假如实际应用中我们在testview中有UITableView,上面的方式就不能点击UITableViewCell了,除非长按cell再释放(这里我试了一下长按也不太会好使)。
还记得之前说gesture优先级最高的时候的解释么?if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.
所以原因就是父view已经识别了这个满足了tapGesture,然后就cancel了之前传给button的touch,并且不会再传给它任何touch了。
要解决这个问题,需要了解UIGestureRecognizer的
cancelsTouchsInView
属性,在手势识别成功之后,是否cancel传递给响应链的触摸事件:
//default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to
//be sent to the view for all touches or presses recognized as part of this gesture immediately
//before the action method is called.
@property(nonatomic) BOOL cancelsTouchesInView;
我们设置tap的cancelsTouchsInView为NO,就可以让cell的点击同时响应tableView的delegate,并且也响应tapGesture啦~~ 之前的场景下的输出也就变成了:
//A
=========> base view touchs Began
=========> single Tapped
=========> base view touchs Ended
//B
=========> test view touchs Began
=========> single Tapped
=========> test view touchs Ended
//C
=========> single Tapped
=========> click testbtn
//D
=========> test view touchs Began
=========> test view touchs Ended
此时,baseView 和testView上的触摸事件就可以完整执行。
顺便再提一下delaysTouchesBegan
属性,也就是只有手势识别失败以后才继续传递事件,如果识别成功响应链就不会拿到touch:
// default is NO. causes all touch or press events to be delivered to
//the target view only after this gesture has failed recognition. set to
//YES to prevent views from processing any touches or presses that
//may be recognized as part of this gesture
@property(nonatomic) BOOL delaysTouchesBegan;
即延迟传递事件给view,我们设置tap的delaysTouchesBegan为YES,那么输出结果就变成:
//A
=========> single Tapped
//B
=========> single Tapped
//C
=========> click testbtn
//D
=========> test view touchs Began
=========> test view touchs Ended
事件先被手势识别了,就不再传递给响应链了。
setExclusiveTouch
如果页面有两个按钮,你用双指同时点击这两个按钮,则会有两个相应发生(模拟器可以用按住option实现双指同时点击)
button1 = [UIButton buttonWithType:UIButtonTypeCustom];
button1.frame = CGRectMake(100, 400, 100, 100);
[button1 setTitle:@"click here" forState:UIControlStateNormal];
[button1 setBackgroundColor:[UIColor greenColor]];
[[button1 rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
NSLog(@"button1 clicked");
}];
// [button1 setExclusiveTouch:YES];
[self.view addSubview:button1];
button2 = [UIButton buttonWithType:UIButtonTypeCustom];
button2.frame = CGRectMake(200, 400, 100, 100);
[button2 setTitle:@"click here" forState:UIControlStateNormal];
[button2 setBackgroundColor:[UIColor yellowColor]];
[[button2 rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
NSLog(@"button2 clicked");
}];
// [button2 setExclusiveTouch:YES];
[self.view addSubview:button2];
现在当你双指点击就会触发:
2020-08-18 07:00:18.438975+0800 Example1[64435:2117934] button2 clicked
2020-08-18 07:00:18.439808+0800 Example1[64435:2117934] button1 clicked
但是如果你解开注释setExclusiveTouch:YES
,每次就只会触发一次button click的回调,而非两次。
这个属性就是一个view在结束独占事件之前不允许其他事件的发生,以避免同时相应多个事件导致弹好几个弹窗重叠、两个button离得太近导致点击同时相应
等问题。
6. Something also nothing
看到的想到的好玩的小问题~~
如何让子view和父view都响应事件
=>如果是gesture的话实现delegate里面的可以和其他gesture同时响应就好,还有就是可以设置子view的gesture的cancelsTouchesInView为NO,都可以实现两个手势同时识别的。为什么子View是UIView时,如果没有添加手势,点击子View时,会由其父View来响应,而子View是 UIControl 时,子View没有添加手势,一样不会由父View来响应
当UIControl跟踪事件的过程中,识别出事件交互符合响应条件,就会触发target-action进行响应。当事件发生时,UIControl监听到需要处理的交互事件时,会调用 sendAction:to:forEvent:
将target、action以及event对象发送给全局应用,Application对象再通过 sendAction:to:from:forEvent:
向target发送action。
=> 所以其实UIControl有覆写touch的四个方法,来鉴别action,所以我猜当它识别到了action以后就会和手势类似cancel其他的action识别啦。
- 如何让两个重叠button都响应事件
=> 木有想出来,也可以把上面一个button改成不能点然后加个gesture,下面的点击事件就可以触发了,但是感觉没啥意义。本来如果overlap其实视觉上还是上面的会被触发,所以let it go吧,睡了睡了~
Reference:
https://www.jianshu.com/p/b95553303a11
https://www.jianshu.com/p/ca3cd5306668
https://www.jianshu.com/p/481465fc4f2d
http://hongchaozhang.github.io/blog/2015/10/21/touch-event-in-ios/