欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS开发 -- 响应事件

程序员文章站 2024-03-20 14:55:16
...

一、响应过程

在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 aaa@qq.com"红色";
    redView.backgroundColor = [UIColor redColor];
    [self.view addSubview:redView];

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

运行长这样:
iOS开发 -- 响应事件
然后重写响应方法和一个测试方法:

/** 手指按下时响应 */
- (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,控制台输出:
iOS开发 -- 响应事件

从输出看出,响应链顺序是:greenView -> redView -> UIViewController.View -> UIViewController -> UIWindow -> UIApplication -> AppDelegate
iOS开发 -- 响应事件
第一响应者是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 aaa@qq.com"蓝色";
    blueView.backgroundColor = [UIColor blueColor];
    [self.view addSubview:blueView];

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

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

iOS开发 -- 响应事件

然后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:
iOS开发 -- 响应事件
控制台输出可见,先找到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;

}

iOS开发 -- 响应事件
然后点击绿色按钮,
iOS开发 -- 响应事件

响应绿色按钮的事件。
然后我们重写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);
}

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

相关标签: iOS 响应事件