iOS的事件传递和响应机制

目的:了解事件传递过程和响应机制能够帮助处理一些手势冲突,自定义手势等问题

1、事件分类?

1.1 在iOS系统中把事件分为4类事件:

  1. UIEventTypeTouches: 触摸手机屏幕事件
  2. UIEventTypeMotion:手机的摇晃和运动事件。比如摇晃手机,手机陀螺仪感应,该事件由UIKit触发的,因此它不遵守事件响应机制。
  3. UIEventTypeRemoteControl:手机远程控制事件。主要是用来接收耳机等外部设备的命令,目的是用来控制手机的多媒体。
  4. 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的,所以如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件。其流程大致如下。

  1. 当用户触摸屏幕后,系统会将该触摸事件加入到一个由UIApplication管理的队列事件中。
  2. UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常会先发送事件给应用程序的主窗口(keyWindow)。
  3. 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。

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直接这些事件传递给指定的对象。

**注:
**新手的文章可能有误,请指正。

你可能感兴趣的:(iOS的事件传递和响应机制)