iOS开发 -- 响应事件

一、响应过程

在iOS中UIResponder类是专门用来响应用户的操作处理各种事件的,包括触摸事件(Touch Events)、运动事件(Motion Events)、远程控制事件(Remote Control Events)。而我们常用的可以响应事件的几个类:UIViewController,UIView,UIApplication都是直接继承自UIResponder。So,跟着来了解下。

在UIResponder中有四个方法:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//开始触摸
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//触摸移动
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//触摸结束
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;//触摸取消

所有进行自定义触摸处理的响应器都应该覆盖这四种方法,
我们用这四个方法来帮助我们了解下响应过程。
先写个视图,CustomView是自定的一个UIView:

- (void)viewDidLoad {
    [super viewDidLoad];
      CustomView *redView = [[CustomView alloc] initWithFrame:CGRectMake(10, 100, 300, 300)];
    redView.name =@"红色";
    redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:redView];

    CustomView *greenView = [[CustomView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    greenView.name =@"绿色";
    greenView.backgroundColor = [UIColor greenColor];
    [redView addSubview:greenView];
}

运行长这样:
这里写图片描述
然后重写响应方法和一个测试方法:

/** 手指按下时响应 */
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    [super touchesBegan:touches withEvent:event];
    NSLog(@"--->手指按下时响应");
    [self eventGoWhere:[touches anyObject].view];

}
- (void)eventGoWhere:(UIView *)view {
    // 遍历响应者链。返回第一个找到视图控制器
    UIResponder *responder = view;
    NSLog(@"responder ---   %@",responder);
    while ((responder = [responder nextResponder])){
         NSLog(@"responder nextResponder--- %@",responder);
    }
}

然后 点击greenView,控制台输出:
这里写图片描述

从输出看出,响应链顺序是:greenView -> redView -> UIViewController.View -> UIViewController -> UIWindow -> UIApplication -> AppDelegate
这里写图片描述
第一响应者是greenView,如果greenView不能处理响应事件,那就传递给nextResponder。如果一个View有一个视图控制器(View Controller),它的下一个响应者是这个视图控制器,紧接着才是它的父视图(Super View),如果一直到Root View都没有处理这个事件,事件会传递到UIWindow(iOS中有一个单例Window),此时Window如果也没有处理事件,便进入UIApplication,UIApplication是一个响应者链的终点,它的下一个响应者指向nil,以结束整个循环。

ok,那我们来思考一下,当我们的手指触摸到屏幕的时候,系统是如何找到第一个响应者的呢?
事实上,iOS系统检测到手指触摸(Touch)操作时,会将其放入当前活动Application的事件队列。UIApplication会从事件队列中取出触摸事件,并传递给key window(当前接收用户事件的窗口)处理。window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,称之为hit-test view。

hitTest:withEvent:方法 什么时候调用?
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
作用:寻找并返回最合适的view(能够响应事件的那个最合适的view)
事件传递给谁,就会调用谁的hitTest:withEvent:方法。
如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

我们来测试一下这个过程,再加几个视图:

CustomView *blueView = [[CustomView alloc] initWithFrame:CGRectMake(10, 450, 300, 200)];
    blueView.name =@"蓝色";
    blueView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:blueView];

    CustomView *purpleView = [[CustomView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
    purpleView.name =@"紫色";
    purpleView.backgroundColor = [UIColor purpleColor];
    [blueView addSubview:purpleView];

    CustomView *grayView = [[CustomView alloc] initWithFrame:CGRectMake(60, 60, 50, 50)];
    grayView.name =@"灰色";
    grayView.backgroundColor = [UIColor grayColor];
    [blueView addSubview:grayView];

这里写图片描述

然后Custome里重写hitTest:withEvent:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"%@",self.name);

    //判断是否合格
    if (!self.hidden && self.alpha > 0.01 && self.isUserInteractionEnabled) {
        BOOL pointInside =[self pointInside: point withEvent:event];
        if (pointInside) {
              NSLog(@"%@ -- YES",self.name);
        }else{
            NSLog(@"%@ -- NO",self.name);
        }

    }
    return [super hitTest:point withEvent:event];
}

注:hitTest:withEvent:方法忽略隐藏(hidden=YES)的视图,禁止用户操作(userInteractionEnabled=YES)的视图,以及alpha级别小于0.01(alpha<0.01)的视图。
这时候再点击purpleView:
这里写图片描述
控制台输出可见,先找到blueView,发现pointInside:withEvent:返回为YES,然后查找其子视图grayView,返回NO,再找到了另一个子视图purpleView,返回YES并且其没有子视图。
hitTest 的顺序如下:

总结hitTest:withEvent:方法的处理流程如下:
1.在顶级视图(Root View)上调用pointInside:withEvent:方法判断触摸点是否在当前视图内;;
2.若返回NO,则hitTest:withEvent:返回nil;
3.若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
4.若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
5.如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。
系统就是这样通过hit test找到触碰到的视图(Initial View)进行响应。

当我们手指触摸屏幕是,系统先找寻Initial View,即Hit-Test 机制:
顺序为:UIApplication -> UIWindow -> Root View -> ··· -> subview
然后找到了Initial View后,如果Initial View并没有或者无法正常处理此次Touch。这个时候,系统就会通过响应者链寻找下一个响应者,也就是前面那个顺序:
Initial View -> View Controller(如果存在) -> superview -> · ·· -> rootView -> UIWindow -> UIApplication
这就是整个响应过程,是两个相反的顺序。

总结一下视图没有响应的击中情况:
1.userInteractionEnabled=NO;
2.hidden=YES;
3.alpha=0~0.01;
4.没有实现touchesBegan:withEvent:方法,直接执行touchesMove:withEvent:等方法;
5.目标视图点击区域不在父视图的Frame上 (superView背景色为clear Color的时候经常会忽略这个问题)。

实际应用,事件拦截

拦截事件的处理
因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。

我们来测试一下,两个子视图,一个在下面被上面一个覆盖,但是点击在下面一个视图的范围内时,希望响应的是下面视图的事件。
在CustomView里加两个Button

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {

        //红色
        UIButton *btn1 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn1.frame = CGRectMake(0, 100, 300, 300);
        btn1.backgroundColor = [UIColor redColor];
        [btn1 addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
        btn1.tag = 1;
        [self addSubview:btn1];

        //绿色
        UIButton *btn2 = [UIButton buttonWithType:UIButtonTypeCustom];
        btn2.frame = CGRectMake(50, 200, 200, 300);
        btn2.backgroundColor = [UIColor greenColor];
        [btn2 addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
        btn2.tag = 2;
        [self addSubview:btn2];


    }
    return self;

}

这里写图片描述
然后点击绿色按钮,
这里写图片描述

响应绿色按钮的事件。
然后我们重写hitTest

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *result = [super hitTest:point withEvent:event];
    CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
    if ([underButton pointInside:buttonPoint withEvent:event]) {
        return underButton;
    }
    return result;
}

- (void)buttonClick:(UIButton *)button
{
    NSLog(@"--- %ld",button.tag);
}

然后我们点击被覆盖重叠的部分,和只有绿色按钮的部分。
这里写图片描述
所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!

猜你喜欢

转载自blog.csdn.net/funnyS__l/article/details/80047162