iOS底层原理之事件的传递与响应

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Bolted_snail/article/details/82852428

iOS中的事件

iOS的事件分为3大类型:触摸事件、加速计事件、远程控制事件;而我们最常用到的是触摸事件。

UIResponder(响应者对象)

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

UIResponder常用API

事件的处理API

//UIResponder内部提供了以下方法来处理事件触摸事件
// 一根或者多根手指开始触摸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;

//加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

//远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

以上方法是由系统自动调用的,所以可以通过重写该方法来处理一些事件。

UITouch(触摸事件对象)

  • 当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch对象,一根手指对应一个UITouch对象;
  • 如果两根手指同时触摸一个视图,那么视图只会调用一次的touchesBegan:withEvent:方法方法,触摸参数中装着2个UITouch对象;
  • 如果这两根手指一前一后分开触摸同一个视图,那么视图会分别调用2次的touchesBegan:withEvent:方法方法,并且每次调用时的触摸参数中只包含一个UITouch对象。

UITouch的作用

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

UITouch常见API

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

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

注意 : UITouch对象是当触摸时系统自动创建的,自己alloc创建是没有意义的。

iOS事件的产生和传递

事件的产生

  • 发生触摸事件后,系统会将该事件加入到一个由UIApplication的管理的事件队列中(FIFO,先进先出),先产生的事件先处理。
  • 的UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口keyWindow
  • 主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
  • 找到合适的视图控件后,就会调用视图控件的触摸方法来作具体的事件处理。

事件的传递

那么如何找到最合适的视图呢?这就看时间是如何传递的!

  • 首先判断主窗口(keyWindow)自己是否能接受触摸事件;

  • 判断触摸点是否在自己身上;

  • 如果上面两部都满足,就将子控件数组中从后往前遍历子控件,让子控件重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1,2-步骤);

  • 如果一个控件自己满足上面的条件,而它的所有子控件都不满足上面条件,或者其没有子控件,则该控件就是响应事件的最合适的视图。

  • UIView的不能接收触摸事件的三种情况:
    alpha <0.01;
    userInteractionEnabled = NO;
    hidden = YES;

  • 注意:采取从后往前遍历子控件的 方式寻找最合适的视图只是为了做一些循环优化。因为相比较之下,后添加的视图在上面,降低循环次数。

寻找最合适的视图底层剖析

寻找响应事件的最合适的视图,需要用到两个关键的方法:

// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
  • 只要事件一传递给一个控件,这个控件就会调用他自己的则hitTest:withEvent:方法方法,寻找并返回最合适的视图(能够响应事件的那个最合适的视图);

  • 实例:
    视图结构如下:GrayViewview的子视图,RedView、YellowViewGrayView的子视图,BlueView、GreenViewRedView的子视图,PurpleView,CyanViewYellowView的子视图;并且添加顺序是从上到下,从左到右。
    视图结构
    在每个自定义的子视图中重新- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)even方法;

// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"事件传递到%@",NSStringFromClass([self class]));
    // 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.从后往前遍历自己的子控件,因为后添加进来的视图一般在最上面,所以从后往前取出子视图,使得遍历效率提高
    NSInteger count = self.subviews.count;
    for (NSInteger i = count - 1; i >= 0; i--) {
        UIView * childView = self.subviews[i];
        // 把当前控件上的坐标系转换成子控件上的坐标系
        CGPoint childP = [self convertPoint:point toView:childView];
        //子控件在重复调用自己的hitTest方法
        UIView *fitView = [childView hitTest:childP withEvent:event];
        // 如果子视图是最合适的就返回
        if (fitView) {
            return fitView;
        }
    }
    // 循环结束,说明只有自己是最合适的view
    return self;
}




//开始点击事调用,及响应事件处理
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%@是最佳的事件响应者",NSStringFromClass([self class]));
}

当点击GrayView时打印结果:
点击了GrayView
当点击BlueView时打印结果:
点击了BlueView

  • return nil的含义:
    hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的视图不是合适的视图,子控件也不是合适的视图。如果同级的兄弟控件也没有合适的视图,那么最合适的视图就是父控件。

  • 截事件的处理:
    正因hitTest:withEvent:方法方法可以返回最合适的视图,所以可以通过重写hitTest:withEvent:方法方法,返回指定的视图作为最合适的图去响应事件。

  • 想让谁成为最合适的视图就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法return self。但是,建议在父控件的则hitTest:withEvent:方法中返回子控件作为最合适的观点!

  • 列如不管点击哪个视图都让YellowView成为处理事件最合适的view,只需要将其俯视图(GrayView)的hitTest:withEvent:方法重写,返回YellowView

// 重写GrayView的hitTest:withEvent:方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"事件传递到%@",NSStringFromClass([self class]));
    // 1.判断当前控件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    for (UIView * childView in self.subviews) {
        if ([childView isMemberOfClass:NSClassFromString(@"YellowView")]) {
            return childView;
        }
    }
    return self;
}

或者重写YellowViewhitTest:withEvent:返回self

//重写YellowView的该方法
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    return self;
}

注意: 因为YellowView后添加到GrayView,所以会优先将事件传递给它,这样可以实现点击任意视图YellowView就是最合适的视图,但是如果想让RedView成为最合适的视图,只重写RedViewhitTest: withEvent:方法并返回self,当点击YellowView或者YellowView的子视图PurpleViewCyanView时返回的最合适视图并不是RedView,所以要拦截某个视图为最合适的视图最好重写其俯视图的hitTest: withEvent:方法。

  • 综上事件的传递顺序是这样的:
      触摸事件 - > UIApplication事件队列 - > [UIWindow hitTest:withEvent:] - >返回更合适的视图 - > [子控件hitTest:withEvent:] - >返回最合适的视图

事件的响应

当事件产生并传递找到最合适的控件,就会调该用控件的触摸方法来作具体的事件处理(也就是响应该事件),如果该控件没有响应触摸事件(有没有重写touchesBegantouchesMovedtouchesEndedtouchesCancelled这些响应触摸事件的方法),这些触摸方法的默认做法是将事件顺着响应者链条向上传递(也就是触摸方法默认不处理事件,只传递事件)。

响应者链

  • 响应者链条示意图
    响应者链
    响应者链是由多个响应者对象连接起来的链条。
  • 响应者链的事件响应传递过程:
  1. 如果当前视图是控制器的视图,那么控制器就是下一个响应者,如果当前视图不是控制器的视图,那么父视图就是当前视图的下一个响应者;
  2. 如果当前响应者不处理事件,就将事件传递给下一个响应者,以此类推;
  3. 在视图层次结构的底顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给窗口对象进行处理;
  4. 如果窗口对象也不处理,则其将事件或消息传递给UIApplication的对象;
  5. 如果UIApplication的也不能处理该事件或消息,则将其丢弃。
  • 事件响应的底层原理:
    只要点击控件,就会调用touchBegin等那4个触摸事件响应方法,如果没有重写这个方法,自己处理不了触摸事件,系统默认就会用supertouchesBegan方法,直到有响应者重写过改法后,不再传递该触摸事件了。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event]; 
}

nextResponder

UIResponder具有nextResponder属性,也就是其SuperView或是UIViewConterller等,这个属性有时候还是很有用的,比如UIViewConterllerView的添加一个UITableView,点击自定cell跳转别的控制器,这时候就可以通过cell.superView.superView.nextResponder就可以获取控制器,然后进行跳转。

总结

  • 事件的传递:即寻找最合适的视图的过程,当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication - > UIWindow - > UIView - >初始视图;
  • 事件的响应:即处理事件,当找到最合适的视图后,先看该视图能不能处理该事件(即有没有有重写几个触摸方法),重写了就不再往下传递,没重写就顺着事件响应者链从子控件传给父控件去查找没有响应者去处理事件,如果都没有处理该事件,则该事件不被任何响应者响应,就抛弃该事件。
  • 事件产生传递处理的整体过程:
    当用户点击一个UIView时,系统会产生一个事件,并将其放入UIApplication的事件队列中。然后该事件会顺着这条链传递到用户点击的那个UIView:UIApplication->UIWindow->RootView->...->Subview。然后开始处理这个事件,若Subview不处理,事件将会传递给视图控制器,若没有控制器则传给其superView,最后传给UIWindow,UIApplication。若UIApplication还是没处理则将事件传给nil。

猜你喜欢

转载自blog.csdn.net/Bolted_snail/article/details/82852428