欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS的事件传递和响应机制

程序员文章站 2024-03-20 13:21:34
...
目的:了解事件传递过程和响应机制能够帮助处理一些手势冲突,自定义手势等问题

###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<UITouch *> *)touches withEvent:(UIEvent *)event方法,touches参数中装着2个UITouch对象。如果两根手机一前一后触摸屏幕view会调用touchesBegan:(NSSet<UITouch *> *)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 <UIResponderStandardEditActions>
	// 获取下一个响应链
@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<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;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches ;

// 手机自带物理按键对应的状态
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event ;
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event ;
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event ;
- (void)pressesCancelled:(NSSet<UIPress *> *)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 <UIGestureRecognizerDelegate> delegate; // the gesture

2、事件产生和传递

2.1 事件传递规则:事件的传递是从父view传到子view的,所以如果父控件接受不到触摸事件,那么子控件就不可能接收到触摸事件。其流程大致如下。

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

2.1 例如,下图是view的层次结构。

iOS的事件传递和响应机制

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:

- (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所示 苹果官网介绍的响应链介绍的例子。

iOS的事件传递和响应机制

                                            图3.1 点击事件响应链示意图
       **解释:**如果子View(UILabel、UITextField、UIBUtton)不处理事件,UIKit发送事件到父UIView对象,然后是窗口的根视图(UIWindow)。在将事件定向到窗口之前,响应器链从根视图转移到所属的视图控制器。如果窗口不能处理事件,UIKit将事件传递给UIApplication对象,特别的,如果该对象是UIResponder的实例,并且不是responder链的一部分,可能还会传递给应用委托。(如果能处理则停止事件传递)。
      **特殊事件:**与加速度计、陀螺仪和磁强计相关的运动事件不遵循响应链。而是由Core Motion直接这些事件传递给指定的对象。

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