iOS事件的传递和响应

问题

  • 事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)

  • 找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)

  • 其中重点和难点是:

    • 1.如何寻找最合适的view
    • 2.寻找最合适的view的底层实现(hitTest:withEvent:底层实现)

iOS中的事件

  • 触摸事件 【本文只讨论触摸事件】
  • 加速计事件
  • 远程控制事件
  • 响应者对象(UIResponder)
    在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。

  • UIViewController

@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>
  • UIView
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate>

事件的处理往往需要 UITouch 对象

事件的产生和传递

    1. 事件的产生
      点击一个UIView或产生一个触摸事件A,那么这个事件就产生了,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列)
    1. 事件的传递
    • 2.1 触摸事件的传递就是把事件传递到处理该事件最合适的view上
      注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

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

      2)判断主窗口(keyWindow)自己是否能接受触摸事件,如果能,则判断触摸点是否在keyWindow身上,如果在,则UIWindow将事件向下分发,即UIView

      3)判断触摸点是否在自己身上,若果在,怎执行第4步

      4)子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行3步骤)

      4)view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。

      6)如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。

  • 2.3 寻找最合适的view底层剖析

    • 两个最重要的方法
      hitTest:withEvent:方法, pointInside方法

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

    hitTest:withEvent 的说明

    • 调用的条件 :只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
    • 作用 :寻找并返回最合适的view 能够响应事件的那个最合适的view

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

    • 拦截事件的处理
      正因为hitTest:withEvent:方法 的返回值为:响应事件的那个最合适的view,所以,通过 复写 该方法可以 拦截事件

    新建一个MView,在.m 文件实现如下:

      // 开始触摸时就会调用一次这个方法
      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
          NSLog(@"摸我干啥!");
      }
      // 手指移动就会调用这个方法
      // 这个方法调用非常频繁
      - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
          NSLog(@"哎呀,不要拽人家!");
      }
      // 手指离开屏幕时就会调用一次这个方法
      - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
          NSLog(@"手放开还能继续玩耍!");
      }
    
      
      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      {
          NSLog(@"point.x = %f",point.x);
          NSLog(@"point.y = %f",point.y);
          NSLog(@"%@",event);
          return nil;
      }
    
    

    上述创建的view 对象点击 输出如下

     point.x = 70.000000
     point.y = 48.666672
     <UITouchesEvent: 0x600001df1dd0> timestamp: 1620.54 touches: {()}
    
    point.x = 70.000000
    point.y = 48.666672
    <UITouchesEvent: 0x600001df1dd0> timestamp: 1620.54 touches: {()}
    

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

    • 所以事件的传递顺序是这样的
      产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

    • 变形 - 返回 self.superview

      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      {
          NSLog(@"point.x = %f",point.x);
          NSLog(@"point.y = %f",point.y);
          NSLog(@"%@",event);
          return self.superview;
      }
    

    输出与上面的一样
    总结: 即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

    • hitTest:withEvent:方法底层实现
    
      - (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;
      }
    
      // 作用:判断下传入过来的点在不在方法调用者的坐标系上
      // point:是方法调用者坐标系上的点
      //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
      //{
      // return NO;
      //}
    
    • pointInside:withEvent:方法
      pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)
      如果返回YES,代表点在方法调用者的坐标系上;
      返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件
  • UIView不能接收触摸事件的三种情况:

    • 不允许交互:userInteractionEnabled = NO
    • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
    • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
  • 3.事件的响应

    • 触摸事件处理的整体过程

      • 用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
      • 找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…
      • 这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理
    • 如何判断上一个响应者
      如果当前这个view是控制器的view,那么控制器就是上一个响应者
      如果当前这个view不是控制器的view,那么父控件就是上一个响应者

    • 响应者链的事件传递过程:

      • 如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
      • 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
      • 如果window对象也不处理,则其将事件或消息传递给UIApplication对象
      • 如果UIApplication也不能处理该事件或消息,则将其丢弃
      //只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
      // 上一个响应者可能是父控件
      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
          // 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
          [super touchesBegan:touches withEvent:event];
          // 注意不是调用父控件的touches方法,而是调用父类的touches方法
          // super是父类 superview是父控件
      }
    

总结

  • 事件的传递与响应:
    • 事件的传递 当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

    • 事件的响应 首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

    • 在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

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

      - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
          // 1.自己先处理事件...
          NSLog(@"do somthing...");
          // 2.再调用系统的默认做法,再把事件交给上一个响应者处理
          [super touchesBegan:touches withEvent:event]; 
      }
      
    • 事件的传递和响应的区别:
      事件的传递是 从上到下(父控件到子控件)
      事件的响应是 从下到上(顺着响应者链条向上传递:子控件到父控件。

参考博客

猜你喜欢

转载自blog.csdn.net/weixin_33769207/article/details/87583183