目的:了解事件传递过程和响应机制能够帮助处理一些手势冲突,自定义手势等问题
1、事件分类?
1.1 在iOS系统中把事件分为4类事件:
- UIEventTypeTouches: 触摸手机屏幕事件
- UIEventTypeMotion:手机的摇晃和运动事件。比如摇晃手机,手机陀螺仪感应,该事件由UIKit触发的,因此它不遵守事件响应机制。
- UIEventTypeRemoteControl:手机远程控制事件。主要是用来接收耳机等外部设备的命令,目的是用来控制手机的多媒体。
- UIEventTypePresses: 物理按压事件,比如音量、开关物理键。
1.2 UITouch介绍
@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));// 触摸类型
@property(nullable,nonatomic,readonly,strong) UIWindow *window;// 事件产生时所处窗口
@property(nullable,nonatomic,readonly,strong) UIView *view; // 事件产生时所处视图
- (CGPoint)locationInView:(nullable UIView *)view; // 当前触摸的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view; // 上一个位置
1.2.1 上面就是UITouch类的关键字段和方法。
- 当手指触摸屏幕的时候就会创建一个UITouch对象,一根手指对应一个UITouch对象。
- 如果两个手指同时触摸屏幕,view只会调用一次touchesBegan:(NSSet
*)touches withEvent:(UIEvent *)event方法,touches参数中装着2个UITouch对象。如果两根手机一前一后触摸屏幕view会调用touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event两次,每次只有一个UITouch对象。 - 当想要多指同时触摸UIView,并且UIView能够接受到的时候需要手动开启multipleTouchEnabled的值为YES,UIView默认是不支持多点点击的。
1.2.2 UITouch的重要属性和方法介绍
- 可以通过timestamp获取事件产生的间隔,自定义特殊的响应事件。
- tapCount 在一定的时间内点击屏幕的次数,根据这个可自定义双击,三击事件。
- locationInView: 当前触摸点,在视图的位置。
- previousLocationInView:上一次触摸点,在视图的位置。
- 上面的两个方法可以完成view的拖拽功能。
1.3 UIResponder的介绍
UIResponder类继承NSObject,在iOS系统中,只有继承了UIResponder才能够响应事件,在iOS体系中,UIApplication、UIViewController和UIView是直接继承UIResponder类的,所以它们能够直接响应事件。特别的是UIWindow继承UIView所以UIWindow也是可以响应事件。在实际开发中,我们可以重写UIResponder 提供的方法来完成特定的需求。如下是UIResponder的部分源码:
@interface UIResponder : NSObject
// 获取下一个响应链
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
// 指定当前的view能否是第一个响应事件,默认不是
@property(nonatomic, readonly) BOOL canBecomeFirstResponder;
- (BOOL)becomeFirstResponder;
// 能否把当前view注册成为第一响应者,默认是的。
@property(nonatomic, readonly) BOOL canResignFirstResponder; 、
- (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 ;
// 手机自带物理按键对应的状态
- (void)pressesBegan:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event ;
- (void)pressesChanged:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event ;
- (void)pressesEnded:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event ;
- (void)pressesCancelled:(NSSet *)presses withEvent:(nullable UIPressesEvent *)event ;
// 手机摇动的状态
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event ;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event ;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event ;
// 远程状态
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event ;
1.4 UITapGestureRecognizer的介绍
UITapGestureRecognizer类是在iOS3.2才开始提供的,使开发人员更加容易的处理触摸屏幕的事件。UITapGestureRecognizer有7个子类,能够帮助我们处理常见的需求,如用UITapGestureRecognizer可以在UITabelViewCell里的图片识别手势事件。
- UITapGestureRecognizer: 单点或者多点的手势识别器
- UIPinchGestureRecognizer: 缩放手势识别器
- UIRotationGestureRecognizer:旋转手势识别器
- UISwipeGestureRecognizer:滑动手势识别器
- UIPanGestureRecognizer: 平移手势识别器
- UIScreenEdgePanGestureRecognizer: 从屏幕边缘附近开始的平移手势
- UILongPressGestureRecognizer: 长按手势识别器
UIGestureRecognizer的部分源码如下所示:
@interface UIGestureRecognizer : NSObject
// Valid action method signatures:
// -(void)handleGesture;
// -(void)handleGesture:(UIGestureRecognizer*)gestureRecognizer;
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action NS_DESIGNATED_INITIALIZER; // designated initializer
- (instancetype)init;
- (nullable instancetype)initWithCoder:(NSCoder *)coder;
// 为识别手势添加行为
- (void)addTarget:(id)target action:(SEL)action;
// 移除手势行为
- (void)removeTarget:(nullable id)target action:(nullable SEL)action;
// 可以使用state来区分UIGestureRecognizer的7个子类
@property(nonatomic,readonly) UIGestureRecognizerState state; // the current state of the gesture recognizer
// 手势识别的代理能够处理不同手势的行为
@property(nullable,nonatomic,weak) id delegate; // the gesture
2、事件产生和传递
2.1 事件传递规则:事件的传递是从父view传到子view的,所以如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件。其流程大致如下。
- 当用户触摸屏幕后,系统会将该触摸事件加入到一个由UIApplication管理的队列事件中。
- UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)。
- 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。
2.1 例如,下图是view的层次结构。
2.2 根据事件传递规则,按照图2.1的点击例子。
- 点击1:这个触点是在Bview上,那么它的事件分发是:UIApplication-->UIWindow-->Aview-->BView。
- 点击2:这个触点在DView上,那么它的事件分发是:UIApplication-->UIWindow-->Aview-->BView-->C1View-->DView。
- 点击3:这个触点在C2View上,那么它的事件分发是:UIApplication-->UIWindow-->Aview-->BView-->C2View。
2.3 UIView不能处理触摸事件的情况
- 不允许交互:userInteractionEnabled = NO
- 隐藏hidden=yes:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
- 透明度apha:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
注意:根据不能处理触摸事件的情况,满足上面的其中一点,那么触摸事件将不会继续传下去,这时将会由它的父控件来处理。例如对于点击2,假设设置DView不允许交互,即userInteractionEnabled = NO。那么点击2的触摸事件将由C1View来处理。
2.4 问题1:iOS是如何确定最合适的接收控件?
大致流程如下:
1. 主窗口接收到应用程序传递过来的事件后,首先判断自己能否接收手触摸事件。如果能,那么再判断触摸点在不在窗口自己身上,执行步骤二,否则丢弃这个事件。
2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件。(从后往前遍历的个人理解:结合事件响应机制,它与事件传递机制传递的路径相反,向里传递到UIApplication结束。所以此处的从后往前遍历减少了不不要的遍历次数,假设当前view是最适合接收事件的view,那么就不必要遍历该view的父view或者父父view,从而减少遍历次数,而响应事件传递到该view也停止)。
3.遍历到每一个子控件后,再判断是否有子控件,然后重复上面的两个步骤:传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上。
4. 如此循环遍历子控件,直到找到最合适接收事件的view,如果遍历后没有更合适的子控件,那么自己就成为最合适接收事件的view。
2.5 问题1的理论依据
2.5.1 hitTest: withEvent:方法
当事件传递到控件时,无论该控件能不能处理事件,该触点在不在该控件上,该控件首先会调用自己的
- (UIView *)hitTest:(CGPoint)point withEvent: (UIEvent *)event
方法寻找最适合接收事件的view。
官方对这个方法的介绍如下:
- This method traverses the view hierarchy by calling the pointInside: withEvent: method of each subview to determine which subview should receive a touch event.
- If pointInside: withEvent: returns YES, then the subview’s hierarchy is similarly traversed until the frontmost view containing the specified point is found.
- If a view does not contain the point, its branch of the view hierarchy is ignored.
官方建议我们不需要显式调用只需要重写它达到特殊的功能,比如屏蔽子控件接受事件。
由上描述可知:
- 方法hitTest: Event:首先调用的是pointInside: withEvent来判断接受者是否可以接受触摸事件,如果可以在调用子控件的hitTest: Event:,如此重复直到找到最合适接受事件的view。
- 如果pointInside: withEven:返回值是YES,该触摸事件会传到子view,同样会调用子view的hitTest: Event:,所以子view的hitTest: Event:默认情况下都会被调用。
- 重写该方法可以自定义事件传递方式。只要该方法返回nil,该事件就不会往下传递,并且认为父控件是最适合接收事件的view。
2.5.2 方法pointInside: withEven:
返回该触点是否在view里,YES是在,NO不在。
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event;
需要强调的是:如果子view超出父view的bounds.那么超出部分的view将不会接受到触摸事件。
2.6 结合这两个方法可以做一些特别的功能
- 转移响应事件,如点击Aview,让BView响应。
- 点击子view,让父view响应。
- 一个事件可以被多个view响应。
3、响应链传递介绍
上文介绍了事件传递,它的结果找到了最合适接收事件的view。而事件响应是从这最适合的view开始的。官网链接。
3.1 传递规则
如果当前的响应控件不处理事件,那么该事件沿着响应链向上传递,如果响应该事件,则消费该事件,停止在响应链上传递。如果响应链传到UIApplication还没被处理就丢弃。它与查找最合适View的方向相反。如图3.1所示 苹果官网介绍的响应链介绍的例子。
图3.1 点击事件响应链示意图
解释:如果子View(UILabel、UITextField、UIBUtton)不处理事件,UIKit发送事件到父UIView对象,然后是窗口的根视图(UIWindow)。在将事件定向到窗口之前,响应器链从根视图转移到所属的视图控制器。如果窗口不能处理事件,UIKit将事件传递给UIApplication对象,特别的,如果该对象是UIResponder的实例,并且不是responder链的一部分,可能还会传递给应用委托。(如果能处理则停止事件传递)。
特殊事件:与加速度计、陀螺仪和磁强计相关的运动事件不遵循响应链。而是由Core Motion直接这些事件传递给指定的对象。
**注:
**新手的文章可能有误,请指正。