OC_ 事件传递机制和响应者链

序言:翻阅资料,学习,探究,总结,借鉴,谢谢探路者,我只是个搬运工。
参考、转发资料:
http://www.jianshu.com/p/2e074db792ba
http://www.jianshu.com/p/2b34ea0b6762
http://blog.csdn.net/a316212802/article/details/50061317

1. 事件的传递、响应流程。

事件传递机制。首先要有个事件(这里我们我们只讲解最常用的触摸事件),然后怎么这个事件是被谁发现的存储在哪,中间是怎么个传递过程,怎么判断谁能执行谁不能执行,最后谁处理。我想这些就是我们需要理解掌握的。

  • 事件源:
    我们研究的是触摸事件,触摸事件其实也分为很多种。例如
    1. 轻击手势 TapGestureRecognizer
    2. 轻扫手势 SwipeGestureRecognizer
    3. 长按手势 LongPressGestureRecognizer
    4. 拖动手势 PanGestureRecognizer
    5. 捏合手势 PinchGestureRecognizer
    6. 旋转手势 RotationGestureRecognizer

为了方便以下讲述的使用轻击手势。

  • 事件是被谁发现的存储在哪
    当程序中发现触摸事件之后,系统会将事件加入到UIApplication管理的一个任务队列中,以堆的形式存储,先进先出先执行。

    • UIApplication是什么?
      UIApplication对象是应用程序的象征,每个应用都有一个自己的UIApplication对象。在一个iOS程序启动后创建的第一个对象就是UIApplication对象,所以在AppDelegate.m文件中执行了UIApplicationDelegate方法供我们使用,而且UIApplication对象以一个单例的形式创建的,所以在其他文件中都能通过单例的形式获取到UIApplication对象。
  • 中间是怎么个传递过程,怎么判断谁能执行谁不能执行,最后谁处理

  1. 系统会将事件加入到UIApplication管理的一个任务队列中,以栈的形式存储,先进先出先执行。
  2. UIApplication会将事件发送给我们最底层的窗口UIWindow,这里的UIWindow值的是keyWindow(主),也只有显示在keyWindow上的视图才能接受点击事件的响应。
  3. UIWindow将事件发送给控制器(或者视图),如果控制器能处理事件的响应,而且触摸点在自己的身上,那么继续寻找子视图。
  4. 遍历所有的子视图,重复上一步的判断,以此循环,直到条件不满足。
  5. 到了这里表示上一步骤的条件不满足。
- 如果找到的作用点在子视图上,但是子视图的userInteractionEnabled属性设置NO(是否响应的设置),那么这个事件就会被废弃。
- 如果找不到触摸点没有在点击的子视图上,那么这个事件由父视图执行。
- 如果摸点在子视图的区域,但是设置了隐藏或者透明度为0时,那这个事件同样由父视图执行。

这就是判断谁能执行谁不能执行,最后谁处理的理由。

2. 具体如何通过代码判断

  1. hitTest:withEvent:方法
  - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{ 
}

这个方法可以返回最合适的view,什么叫最合适,首先这个方法的返回值,可以是自己,也以是子视图,也或者是nil。在我们不重写这个方法的时候,就要子视图是否满足我们的查询要求。

  1. pointInside:withEvent:方法
    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
  {
  }

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

  1. 内部的实现大概是这样的
  - (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
{
  // 判断点在不在区域内
   if (CGRectContainsPoint(self.button.bounds, tempoint))
  {
      return YES ;
    }
return NO ;
}

3. 响应者链

  • UIResponder作为一个响应者事件,所有的可交互控件都是UIResponder直接或者间接的子类。

  • 响应者链的起点为: 上面我们所寻找到的最最合适的View,而这个View将调用自身的
    touchesBegan:withEvent:方法来开始事件响应者链的起始传递。

  • 添加到屏幕中的视图层级关系,从最上层的可能是button的点击事件,传递给父类或者自身的控制器并调用父类的touchesBegan:withEvent:方法。一直到最底层显示的keyWindow上这就是响应者链的传递。响应者链是由多层视图组成的结构(不管是View,还是控制器都是继承于响应者类UIResponder的)。由最上面的子控件传递给最底层的window。

  • 如何判断上一个响应者

    • 如果当前这个view是控制器的view,那么控制器就是上一个响应者
    • 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
  • 事件响应和响应者链的区别:

    • 事件响应是从最底层的keyWindow开始向上分发事件的,而响应者链是从最合适View开始向下传递的。 方向不同。

4. 实际案例。

  1. button部分视图区域超出了父视图,怎么实现点击超出部分也执行点击效果。
    例子:
// ViewController.m
    - (void)viewDidLoad {
    [super viewDidLoad];
      AView *aView = [[AView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)] ;
    aView.backgroundColor = [UIColor grayColor] ;
    [self.view addSubview:aView] ;
    
    [aView _initViews] ;
  }
  // AView.m
  // 创建试图
  - (void)_initViews
{
    self.button = [UIButton buttonWithType:UIButtonTypeCustom] ;
    self.button.frame = CGRectMake(-50, -50, 100, 100) ;
    self.button.backgroundColor = [UIColor redColor] ;
    [self.button addTarget:self action:@selector(buttoClick:) forControlEvents:UIControlEventTouchUpInside] ;
    [self addSubview:self.button] ;
}
// 点击方法
  - (void)buttoClick:(id)sender
{
    NSLog(@"点击了button") ;
}

如果在AView中不做任何处理,那么


分析图.png
  • 解决办法:
    在AView中重写hitTest:withEvent:方法
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *view = [super hitTest:point withEvent:event];
    if (view == nil) {
/*
       // 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值
       - (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view;
     // 将像素point从view中转换到当前视图中,返回在当前视图中的像素值
       - (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view;
     */
          CGPoint tempoint = [self.button convertPoint:point fromView:self];
        if (CGRectContainsPoint(self.button.bounds, tempoint))
        {
            view = self.button;
          }
      }
      return view;
}  
  ```
当点击的触摸手势在Button的视图上,这样手动判断返回我们指定的View为Button,就可以了。

2. 有时候我们经常有这样的需求,在子视图View中想拿到View所在的控制器进行一些操作,通过事件响应者链寻找子视图所在的控制器。(这里我们抛弃基础的通过superView的方法获取)
首先我们要明白一点就是:可交互控件都是UIResponder直接或者间接的子类。

import "UIView+ViewController.h"

@implementation UIView (ViewController)
/*
为UIView扩展一个类目,通过这个方法可以获取这个视图所在的控制器
*/

  • (UIViewController *)viewController
    {
    // 获取当前对象的下一响应者
    UIResponder *nextResp = self.nextResponder;
    while (![nextResp isKindOfClass:[UIViewController class]] && nextResp != nil) {
    // 获取nextResp对象的下一响应者
    nextResp = nextResp.nextResponder;
    }
    return (UIViewController *)nextResp;
    }
使用方法,例如我们还是在我们刚写的deme中的button点击方法中使用,看看效果,记得导入类别。
  • (void)buttoClick:(id)sender
    {
    NSLog(@"点击了button") ;
    UIViewController *vc = [self viewController] ;
    NSLog(@"%@",[NSString stringWithUTF8String:object_getClassName(vc)]) ;
    // 2017-03-17 10:57:57.289 Init[2384:473093] 点击了button
    // 2017-03-17 10:57:57.290 Init[2384:473093] ViewController
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容