美文网首页iOS底层技术
IOS事件的传递与响应研究

IOS事件的传递与响应研究

作者: wg刚 | 来源:发表于2018-07-30 17:20 被阅读0次

    响应者对象:能处理事件的对象,也就是继承自UIResponder的对象。
    响应者链是由多个响应者对象连接起来的链条。
    只有继承UIResponder的的类,才能处理事件,UIApplication、UIView、UIViewController都是继承自UIResponder类,可以响应和处理事件。CALayer不是UIResponder的子类,无法处理事件。
    用队列管理事件,而不用栈:队列先进先出,能保证先产生的事件先处理。栈先进后出。
    先看一个例子来引出相关概念:
    如图:控制器的view上有一个子视图AView,在AView上有一个子视图BView

    image.png

    我们通过点击事件来打印出最合适的view:

    #import "AView.h"
    
    @implementation AView
    
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        UIView *view = [super hitTest:point withEvent:event];
        if (view == self) {
            NSLog(@"AView上最合适view是%@",@"AView");
            return self;
        }else if(view == self.subviews[0]){
            NSLog(@"AView上最合适view是%@",@"BView");
            return view;
        }else{
            NSLog(@"在AView上没有最合适的view");
            return nil;
        }
    }
    @end
    ==================================================
    #import "BView.h"
    
    @implementation BView
    
    @end
    
    ==================================================
    
    #import "ViewController.h"
    #import "AView.h"
    #import "BView.h"
    
    @interface ViewController ()
    
    @property (nonatomic, strong) AView *aView;
    @property (nonatomic, strong) BView *bView;
    
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.title = @"ViewController";
        
        [self.view addSubview:self.aView];
        [_aView addSubview:self.bView];
    }
    
    -(AView *)aView{
        if (!_aView) {
            _aView = [[AView alloc] init];
            _aView.backgroundColor = [UIColor yellowColor];
            _aView.frame = CGRectMake(20, 100, 200, 200);
        }
        return _aView;
    }
    
    -(BView *)bView{
        if (!_bView) {
            _bView = [[BView alloc] init];
            _bView.backgroundColor = [UIColor greenColor];
            _bView.frame = CGRectMake(0, 0, 100, 100);
        }
        return _bView;
    }
    @end
    

    执行结果:

    333.gif

    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法可以找到最合适的view成为事件的响应者。

    但是他是怎么找到的呢?
    我们队上面代码进行改写:

    ###//在AView.m文件中:
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) {
            NSLog(@"在AView上self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES导致它和其子视图没有最合适的view");
            return nil;
        }
       // 判断点在不在当前控件
        if (![self pointInside:point withEvent:event]) {
            NSLog(@"触摸事件不在AView上");
            return nil;
        }
        NSInteger count = self.subviews.count;
       //从外向里遍历自己的子控件
        for (NSInteger i = count-1; i>=0; i--) {
            UIView *subView = self.subviews[i];
            CGPoint subPoint = [self convertPoint:point toView:subView];
            UIView *suitView = [subView hitTest:subPoint withEvent:event];
            if (suitView) {
                NSLog(@"AView上最合适view是%@",@"BView");
                return suitView;
            }
        }
        NSLog(@"AView上最合适view是%@",@"AView");
        return self;
    }
    
    ###//在BView.m文件中:
    -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
        UIView *view = [super hitTest:point withEvent:event];
        if (view) {
            if (view == self) {
                NSLog(@"BView上最合适view是%@",@"BView");
                return self;
            }else if(view == self.subviews[0]){
                NSLog(@"BView上最合适view是%@",@"CView");
                return view;
            }else{
                NSLog(@"BView上没有最合适的view");
                return nil;
            }
        }else{
            NSLog(@"触摸事件BView上");
            return nil;
        }
    }
    

    分别点击BView、AView和视图控制器对应的view,打印结果分别如下:
    BView:

    image.png

    AView:

    image.png

    view:

    image.png

    我如果把AView的self.userInteractionEnabled设为NO

    点击三个view的运行结果一样:

    image.png

    分析:

    点击BView
    1、事件从上面一步一步传到AView,执行hitTest:withEvent:发现AView符合要求(什么要求在下面总结),然后再开始遍历其子视图,看看是否有更合适的。
    2、BView是AView的唯一子视图,执行BView的hitTest:withEvent:发现符合条件
    3、而且BView上没有子视图,所以其成为最合适view,返回。

    点击AView
    1、事件从上面一步一步传到AView,执行hitTest:withEvent:发现AView符合要求(什么要求在下面总结),然后再开始遍历其子视图,看看是否有更合适的。
    2、BView是AView的唯一子视图,执行BView的hitTest:withEvent:发现不符合条件(触摸点不在BView上)。所以AView就是最合适的view, 返回。

    点击控制器对应的view
    1、首先执行控制器view对应的hitTest:withEvent:,发现符合条件,
    2、view上有子视图AView、BView在AView上。所以遍历view的子视图找到AView,执行AView的hitTest:withEvent:发现不符合条件(触摸点不再AView上)
    3、因为AView已经不符合条件了,所以就不会再去遍历AView的子视图,最终确定控制器对应的view就是最合适的view,返回。

    在AView的self.userInteractionEnabled设为NO或者 _aView.alpha = 0.001;
    或者 _aView.hidden = YES;
    的情况下
    1、事件从上面一步一步传到AView,执行hitTest:withEvent:发现AView不符合要求(userInteractionEnabled为NO或者alpha<=0.01或者隐藏),同时不会再遍历其子视图。

    总结:

    view符合条件必须同时满足:

    1、view的userInteractionEnabled为YES;
    2、view的hidden为NO;
    3、view的alpha大于0.01;

    事件的传递:就是寻找最合适View的过程

    1、当iOS程序中发生触摸事件后,系统会将事件加入到UIApplication管理的一个任务队列中。
    2、UIApplication将处于任务队列最前端的事件向下分发。即UIWindow。
    3、AppDelegate 的 window 收到事件,并开始执行 hitTest:withEvent: ,发现符合要求,开始遍历子view.
    4、View首先看自己是否能处理事件、是否隐藏、触摸点是否在自己身上、如果这三个条件都满足,那么继续遍历寻找子视图。
    5、如果没有找到合适的子视图,则说明自己就是做合适的view。
    7、如果有则执行4步骤。

    事件传递到某视图后,就调用其hitTest:withEvent:方法寻找更合适的view。想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件。
    return nil的意思是调用当前的view不是合适的view。
    image.png
    上图视图层级从外向里顺序依次是 CDA EFB 父视图。当点击界面发生触摸事件时,遍历父视图的子视图AB,从后往前遍历,先遍历A视图。

    事件的响应:

    1、首先看找到的最合适的view能否处理这个事件;
    2、如果不能则会将事件传递给其父视图;
    3、如果父视图仍然无法处理则会继续往上传递;一直传递到视图控制器;
    4、判断视图控制器的根视图view是否能处理此事件:如果不能则接着判断该视图控制器能否处理此事件;
    5、如果还是不能则继续向上传递;
    6、一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃。

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

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
    // 1.自己先处理事件...
    // 2.再调用系统的默认做法,再把事件交给上一个响应者处理
    [super touchesBegan:touches withEvent:event]; 
    }
    

    事件的传递和响应的区别:

    事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递(子控件到父控件)。

    判断上一个响应者nextResponder

    1、 当前view是控制器的view,那么控制器就是上一个响应者
    2、 当前view不是控制器的view,那么父视图就是上一个响应者

    应用:

    在自定义的view中如果想在自己里面做页面push跳转,需要找到最上面的一个UINavigationController

    #import "UIView+WGNavigationController.h"
    
    @implementation UIView (WGNavigationController)
    
    - (UINavigationController *)wgNavigationController{
        //响应连上一个响应者
        UIResponder *responder = self.nextResponder;
        while (responder) {
            if ([responder isKindOfClass:[UINavigationController class]]) {
                return (UINavigationController*)responder;
            }
            responder = responder.nextResponder;
        }
        return nil;
    }
    
    @end
    

    参考文章:
    1、史上最详细的iOS之事件的传递和响应机制-原理篇

    2、iOS 响应链和事件传递

    相关文章

      网友评论

        本文标题:IOS事件的传递与响应研究

        本文链接:https://www.haomeiwen.com/subject/brvomftx.html