触摸事件

触摸事件处理的完整过程

  • 发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列
  • UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常先发送事件给应用程序的主窗口
  • 主窗口会在视图层次结构中找到最合适处理事件的控件,这也是整个事件处理过程的第一步
  • 找到最合适处理事件的控件后,调用控件的touches...方法,touches方法的默认做法是将事件顺着响应者链条向上传递,将事件交给上一个响应者进行处理,直至事件处理完毕

事件传递

  • 触摸事件的传递是从父控件传递到子控件
  • 如果父控件不能接收触摸事件,那么子控件就不可能接收到触摸事件
  • 不能接受触摸事件的四种情况
    • 不能与用户交互,即:userInteractionEnabled = NO
    • 隐藏,即:hidden = YES
    • 透明,即:alpha <= 0.01
    • 未启用,即:enabled = NO
  • 提示:UIImageView的userInteractionEnabled默认就是NO,因此UIImageView以及它的子控件默认是不能接收触摸事件的
  • 如何找到最合适处理事件的控件:
    • 首先,判断自己能否接收触摸事件,可以通过重写hitTest:withEvent:方法验证
    • 其次,判断触摸点是否在自己身上,对应方法pointInside:withEvent:
    • 从后往前(先遍历最后添加的子控件)遍历子控件,重复前面的两个步骤
    • 如果没有找到符合条件的子控件,那么就自己处理

响应者链条

  • 定义:是由多个响应者对象连接起来的链条
  • 响应者:能处理事件的对象
  • 作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件可以由多个对象处理
  • 如何判断上一个响应者

    • 如果当前这个view控制器的view,那么控制器就是上一个响应者
    • 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
    • 注意:继承于UIControl的控件有些特殊,下面用代码解释

      -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
      {
        // 按照正常情况会将事件传递给其父控件
        // 但由于UIControl本身有处理事件的能力,所以事件【被拦截】,不会传递给其父控件
        [super touchesBegan:touches withEvent:event];
      
        // 如果真想把事件传递给父控件,可以这样做
        // [self.nextResponder touchesBegan:touches withEvent:event];
      }
      
  • 响应者链条的传递过程
    • 如果控制器的view,就传递给控制器
    • 如果不是控制器的view,则传递给它的父控件
    • 如果视图层次结构的最顶层视图也不能处理收到的事件或消息,则会将事件或消息传递给UIWindow对象进行处理
    • 如果UIWindow对象也不能处理,则将事件或消息传递给UIApplication对象
    • 如果UIApplication也不能处理该事件或消息,则将其丢弃

验证事件传递和响应者链条的默认做法

  • 模仿系统做法找到最合适的控件的做法

      - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      {
          NSLog(@"%@---%s",NSStringFromClass([self class]),__func__);
    
          // 系统默认做法
      //    return [super hitTest:point withEvent:event];
    
          // 模仿系统做法找到最合适的控件的做法
          return [self findTheRightView:point withEvent:event];
      }
    
      /**
       *  模仿系统做法找到最合适的控件的做法
       */
      - (UIView *)findTheRightView:(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.从后往前遍历自己的子控件
          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];
              // 递归找到最合适处理事件的控件
              UIView *rightView = [childView findTheRightView:childP withEvent:event];
    
              if (rightView)return rightView;
          }
    
          // 循环结束,表示没有比自己更合适的处理事件的view
          return self;
      }
    
  • hitTest:withEvent:方法妙用:可以重写这个方法来改变系统默认寻找最合适处理事件的view的处理逻辑(比如:可以控制一个按钮始终是最合适处理事件的控件,不管他上面是否有其他覆盖控件)
  • pointInside:withEvent:方法妙用:可以重写这个方法来改变系统默认判断点是否在自己身上的逻辑
  • pointInside:withEvent:方法与CGRectContainsPoint的区别

    • CGRectContainsPoint用于判断一个点是否在指定区域,左上角参考点(0,0)必须一致才有可比性
    • pointInside:withEvent:方法用于判断一个点(相对指定控件的左上角的(0,0)点确定的坐标)是否在当前控件内

      /**
      *  控制返回最合适处理事件的控件
      */
      -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
      {
        // 将当前坐标点转化为相对按钮左上角(0,0)坐标点
        CGPoint btnPoint = [self convertPoint:point toView:self.btn];
      
        // CGRectContainsPoint用于判断一个点是否在指定区域,左上角参考点(0,0)必须一致才有可比性
        // 由于btnPoint已经被转化为相对按钮的左上角的坐标点了,所以不能用此函数判断点是否在指定区域内
      //    BOOL isContain = CGRectContainsPoint(self.btn.frame, btnPoint);
      //    NSLog(@"self.btn.frame-->%@",NSStringFromCGRect(self.btn.frame));
      //    NSLog(@"btnPoint-->%@",NSStringFromCGPoint(btnPoint));
      
        // 只要点在按钮上就交由按钮处理对应的事件
        if([self pointInside:btnPoint withEvent:event])
        {
            return self.btn;
        }
      
        return [super hitTest:point withEvent:event];
      }
      

results matching ""

    No results matching ""