事件传递

事件传递流程

  • 发生触摸事件后,系统会将该事件封装成UIEvent对象加入到一个由UIApplication管理的事件队列

  • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)

  • 主窗口会调用hitTest:withEvent: 方法沿着视图层次结构从上到下进行传递最后在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步

  • 找到合适的视图控件后,就会调用视图控件的touches方法(touchesBegan、touchesMoved、touchedEnded)来作具体的事件处理

触摸事件的传递是从父控件传递到子控件

也就是UIApplication->window->寻找处理事件最合适的view

触摸事件的传递是从父控件传递到子控件,如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

img

如何找到最合适的控件来处理事件?

  • 自己是否能接收触摸事件?

  • 触摸点是否在自己身上?

  • 从后往前遍历子控件,重复前面的两个步骤

  • 如果没有符合条件的子控件,那么就自己最适合处理

UIView不接收触摸事件的三种情况:

  • userInteractionEnabled = NO隐藏

  • hidden = YES;

  • 透明:alpha = 0.0 ~ 0.01;

通过pointInside:withEvent 方法判断触摸点是否在自己身上。返回NO则不在自己身上,那就不再遍历子控件,返回YES,代表在自己身上,那就继续遍历子控件,从后往前遍历子控件,重复前面两个步骤如果没有符合条件的子控件,那么自己就是最适合处理的控件找到“最合适” 接收的控件后,调用控件touchesBegan,touchesMoved,touchedEnded的方法。

事件传递示例

img

  • 点击了绿色的view:UIApplication -> UIWindow -> 白色 -> 绿色

  • 点击了蓝色的view:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色

  • 点击了黄色的view:UIApplication -> UIWindow -> 白色 -> 橙色 -> 蓝色 -> 黄色

寻找最合适的控件底层剖析

这里用到了两个重要的方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

hitTest:withEvent

只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法

为了寻找并返回最合适的view(能够响应事件的那个最合适的view)

下面是其实现逻辑:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1.判断下窗口能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2.判断下点在不在窗口上
    // 不在窗口上
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 3.从后往前遍历子控件数组
    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) {
            // 如果能找到最合适的view
            return fitView;
        }
     }
     // 4.没有找到更合适的view,也就是没有比自己更合适的view
     return self;
 }

  • 首先判断下窗口能否接收事件

  • 接着调用当前视图的pointInside:withEvent: 方法判断触摸点是否在当前视图内

  • 若返回NO,则hitTest:withEvent: 返回 nil。

  • 若返回YES,则向当前视图的所有子视图发送hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶视图一直到最低层视图,即从 subviews 数组的末尾向前遍历,直到有子视图返回非空对象,或者全部子视图遍历完毕。

  • 若第一次有子视图返回非空对象,则hitTest:withEvent:返回此对象,处理结束。

  • 若所有子视图都返回空,则hitTest:withEvent:返回自身

不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法

如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

pointInside:withEvent

判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

事件响应

事件响应流程

  • 如果找到最合适的控件来处理调用最合适的控件的touches…(touchesBegan、touchesMoved、touchedEnded)方法。

  • 如果调用了[super touch…],就会将事件顺着响应者链往上传递,传给上一个响应者,接着上一个响应者就会调用touches…方法。

  • 如果没有找到合适的控件来处理事件,则将事件传回来窗口,窗口不处理事件,将事件传给 UIApplication。如果 UIApplication 不能处理事件,则将其丢弃。

img

  • 系统首先检查当前触摸到的视图是否响应事件,如果响应事件传递结束,否则转步骤2

  • 系统检查当前触摸到的视图的控制器,如果控制器响应则事件传递结束;如果该视图没有控制器或者控制器不响应该事件,则转步骤3

  • 系统检查父视图,再检查父视图的控制器,以此类推

  • 最后,如果最顶层的视图/控制器也不响应则交给window

响应者

在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接收并处理事件,称之为“响应者对象”。

UIApplication、UIViewController、UIView都继承自UIResponder,因此它们都是响应者对象,都能够接收并处理事件。

UIResponder提供了我们平时最常用的touchesBegan/touchesMoved/touchesEnded方法。此外还有如下几个属性比较重要:

  • isFirstResponder:判断该View是否为第一响应者。

  • canBecomeFirstResponder:判断该View是否可以成为第一响应者。

  • becomeFirstResponder:使该View成为第一响应者。

  • resignFirstResponder:取消View的第一响应者。

第一响应者和最佳响应者
  1. 第一响应者 (First Responder):

    • 第一响应者是指当前能够响应某个事件的第一个对象。
    • 通常情况下,当某个事件发生时,该事件首先被传递到第一响应者。
    • 第一响应者通常是用户当前正在交互的视图,比如用户正在编辑的 UITextField或者点击的 UIButton

    事件传递的目的就是为了让我们找到第一响应者

    如何判断第一响应者:

    1. 能够响应触摸事件
    2. 触摸点在自己身上
    3. 没有任何子视图,或是所有子视图都不在触摸点上
  2. 最佳响应者 (Best Responder):

    • 最佳响应者是指在响应者链上最适合处理某个事件的对象。
    • 当第一响应者无法完全处理某个事件时,该事件会沿着响应者链向上传递,直到找到最佳响应者。
    • 最佳响应者通常是能够最完整地处理该事件的对象,比如包含第一响应者的视图控制器。

一般来说,最佳响应者往往是包含当前第一响应者的视图控制器。

什么是上一个响应者

如果当前这个view是控制器的view,那么控制器就是上一个响应者;
如果当前这个view不是控制器的view,那么父控件就是上一个响应者。

响应者链条是什么

它是一种事件处理机制,由多个响应者对象连接起来的层次结构,使得事件可以沿着这些对象进行传递。利用响应者链条我们可以通过调用touches的super 方法,让多个响应者同时响应该事件。

如何做到一个事件多个对象处理

因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的

iOS中的各种事件

iOS中的事件可以分为三种类型:

  • 触摸事件
  • 加速计事件
  • 远程控制事件

img

触摸事件(UITouch)

保存着跟手指相关的信息,比如触摸的位置、时间、阶段。 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。 当手指离开屏幕时,系统会销毁相应的UITouch对象。

UITouch的常用属性和方法

@property(nonatomic,readonly,retain) UIWindow *window;
//触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIView *view;
//触摸产生时所处的视图
@property(nonatomic,readonly) NSUInteger tapCount;
//短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSTimeInterval timestamp;
//记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) UITouchPhase phase;
//当前触摸事件所处的状态

- (CGPoint)locationInView:(UIView *)view;
//返回值表示触摸在view上的位置,这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0));调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置。
- (CGPoint)previousLocationInView:(UIView *)view;
//该方法记录了前一个触摸点的位置。

UIEvent

UIEvent:称为事件对象,记录事件产生的时刻和类型。 每产生一个事件,就会产生一个UIEvent对象。 UIEvent还提供了相应的方法可以获得在某个view上面的触摸对象(UITouch)。

@property(nonatomic,readonly) UIEventType type;
@property(nonatomic,readonly) UIEventSubtype subtype;
//事件类型
@property(nonatomic,readonly) NSTimeInterval timestamp;
//事件产生的时间

触摸过程

一次完整的触摸过程,通常会经历3个状态:

  • 触摸开始:- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  • 触摸移动:- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
  • 触摸结束:- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
  • 触摸取消(可能会经历):- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event

4个触摸事件处理方法中,都有NSSet *touchesUIEvent *event两个参数。

当用户用手指触摸屏幕时,会创建一个与手指相关联的UITouch对象。一根手指对应一个UITouch对象。

  • 一次完整的触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数。
  • 如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象。
  • 如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象。
  • 根据touches中UITouch的个数可以判断出是单点触摸还是多点触摸。

touches中存放的都是UITouch对象

UIGestureRecognizer(手势识别器)

利用UIGestureRecognizer,能轻松识别用户在某个view上面做的一些常见手势。 UIGestureRecognizer是一个抽象类,定义了所有手势的基本行为,使用它的子类才能处理具体的手势 UITapGestureRecognizer(敲击) UIPinchGestureRecognizer(捏合,用于缩放) UIPanGestureRecognizer(拖拽) UISwipeGestureRecognizer(轻扫) UIRotationGestureRecognizer(旋转) UILongPressGestureRecognizer(长按)

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    // 没有触摸事件发生,所有手势识别的默认状态
    UIGestureRecognizerStatePossible,
    // 一个手势已经开始但尚未改变或者完成时
    UIGestureRecognizerStateBegan,
    // 手势状态改变
    UIGestureRecognizerStateChanged,
    // 手势完成
    UIGestureRecognizerStateEnded,
    // 手势取消,恢复至Possible状态
    UIGestureRecognizerStateCancelled, 
    // 手势失败,恢复至Possible状态
    UIGestureRecognizerStateFailed,
    // 识别到手势识别
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
};

总结

传递链:有系统向最上层view传递,Application -> window -> root view -> … -> first view

响应连:由最基础的view向系统传递,first view -> super view -> … -> view controller -> window -> Application -> AppDelegate

穿透控件:

如果我们不想让某个视图响应事件,只需要重载 PointInside:withEvent:方法,让此方法返回NO就行了.

若是view上有view1,view1上有view2,点击view2,view2自己响应,点击view1,view1不响应,只有view响应,也就是隔层传递

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐