本文介绍: 通过响应事件传递过来的新响应者,将这个事件的具体调用方法(即我们自定义方法传递给了这个新响应者,这样新响应者实现方法调用执行我们自定义的方法。方法来作具体处理,然而这些方法默认都是不做处理的,但是我们要是想让该响应响应事件可以重写一开始说的那几个响应事件方法,并且我们也可以在重写这里我们扩大其响应范围创建button大小是不会变化的,变化的只是我们看不到的其可以响应的范围通过三种处理事件的方法就可以知道事件的整个过程,我们平时经常使用的就是触摸事件,其中有两个参数,……

iOS事件链有两条:
事件的响应链
Hit-Testing事件的传递

432423423

iOS中的三大事件

iOS中的事件类型

423423423

UIKit继承

423423423
通过继承图我们能知道,我们平时在使用的UI大多数都是继承自UIResponder的,只有继承自UIResponder的对象才能接收并处理事件,我们把这类对象称为“响应者”。就像UIApplicationUIViewControllerUIView都继承自UIResponder,因此他们都可以接收处理事件,并且UIResponder中提供了三种处理事件的方法(触摸事件、加速计事件、远程控制事件),所以我们才能在UI中实现各种点击事件:

// 触摸事件
// 开始接触屏幕,就会调用一次
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
// 手指开始移动就会调用(这个方法会频繁的调用,其实一接触屏幕就会多次调用
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
// 手指离开屏幕时,调用一次
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者view上面添加手势时,系统自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

// 加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

// 远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

通过三种处理事件的方法就可以知道事件的整个过程,我们平时经常使用的就是触摸事件,其中有两个参数UITouchUIEvent

除了以上方法,现在还有一个类,UIGestureRecognizer,它是一个抽象类使用它的子类能帮助我们轻松识别view上的各种手势

UITapGestureRecognizer // 敲击
UIPinchGestureRecognizer // 捏合,用于缩放
UIPanGestureRecognizer // 拖拽
UISwipeGestureRecognizer // 轻扫
UIRotationGestureRecognizer // 旋转
UILongPressGestureRecognizer // 长按

UITouch

当你用一根手指触摸屏幕时,会创建一个与之关联的UITouch对象一个UITouch对象对应一根手指。在事件中可以根据NSSet中UITouch对象的数量得出此次触摸事件是单指触摸还是双指多指等等。

UITouch几个重要属性
// 触摸产生时所处的窗口
@property(nonatomic, readonly, retain) UIWindow *window;
// 触摸产生时所处的视图
@property(nonatomic, readonly, retain) UIView *view;
// 短时间内点按屏幕的次数,可以根据tapCount判断单击双击或更多的点击
@property(nonatomic, readonly) NSUInteger tapCount;
// 记录了触摸事件产生或变化时的时间单位是秒
@property(nonatomic, readonly) NSTimeInterval timestamp;
// 当前触摸事件所处的状态
@property(nonatomic, readonly) UITouchPhase phase;
UITouch的两个方法(可用于view的拖拽)
- (CGPoint)locationInView:(UIView *)view;
/*
  返回值表示触摸在view上的位置
  这里返回的位置是针对传入的view的坐标系(以view的左上角为原点(0, 0))
  调用时传入的view参数nil的话,返回的是触摸点在UIWindow的位置
*/

// 该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(UIView *)view;

UIEvent

每产生一个事件,就会产生一个UIEvent事件,UIEvent称为事件对象,记录事件产生的时刻和类型等等。

UIEvent几个重要属性
// 事件类型
@property(nonatomic, readonly) UIEventType type;
@property(nonatomic, readonly) UIEventSubtype subtype;
// 事件产生的时间
@property(nonatomic, readonly) NSTimeInterval timestamp;

事件的产生与传递

传递链

UIApplication传递事件到当前Window是明确的(即一定会的),接下来就是从Window开始找最佳响应视图,此过程有两个重要的方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // 递归调用的事件,从最底层开始,一直往上找,直到找到一个最上层的能响应的视图返回视图
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // 判断点击区域是否在该视图范围中,不在该视图范围中就结束递归返回nil
传递过程
hitTest:withEvent:方法的可能实现
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1.判断当前控件能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha) 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 *fitView = [childView hitTest:childP withEvent:event];
        if (fitView) { // 直到寻找到最合适的view
            return fitView;
        }
    }
    // 循环结束,表示没有自己更合适的view
    return self;
}
注意

响应链

当找到最合适的响应者之后,便会调用控件相应的touch方法来作具体处理,然而这些方法默认都是不做处理的,但是我们要是想让该响应者响应该事件就可以重写一开始说的那几个响应事件方法,并且我们也可以在重写touch方法中加入[super touch],使多个响应者同时响应同一事件。如果我们对响应事件的方法不做处理那么将该事件随着响应者链条往回传递,交给上一个响应者来处理(即调用supertouch方法),直到找到一个能响应该事件的响应者。

响应过程
  • 1.通过hitTest返回的view为当前事件的第一响应者,nextResponder为上一个响应者
  • 2.如果当前view默认不去重写响应事件方法,或者重写调用了父类的响应事件方法,响应就会沿着响应者链向上传递(上一个响应者一般是superView,可以通过nextResponder属性获取上一个响应者)
  • 3.如果上一个响应者是viewController,由viewController的view处理,如果view本身没处理,则传递给viewController本身
  • 4.重复上述过程,直到传递到window,window如果也不能处理,则传递到UIApplication,如果UIApplication的delegate继承自UIResponder,则交给delegate处理,如果delegate也不处理最后丢弃

UIControl的Target-Action设计模式

在 UIControl 及其子类(UIButton等控件)的设计上,iOS Api 采用Target-Action设计模式宏观上来看,这并不属于响应者链的一部分,它只是事件处理的一个末端机制

[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];

这种代码我们几乎天天都在写,这就是典型的Target-Action设计模式

UIControl 通过这种Target-Action方式对 UIEvent 进行了转发,从而可以把 UIEvent 事件转发任意对象处理(原本只有 UIReponder 对象和手势识别器对象才能处理)。

Target-Action设计模式的具体实现

UIControl 实现的具体做法其实是,重写touchesBegan相关方法,通过改变响应者链来实现事件转发的:

4234234

简要概括

简单的来说,Target-Action设计模式其实就是重写了UIControl类的touch响应事件,通过重写的类的touch响应事件将事件转发给了UIApplication子类没有重写touch方法默认就会调用父类的),然后UIApplication通过响应事件传递过来的新响应者,将这个事件的具体调用方法(即我们自定义的方法)传递给了这个新响应者,这样新响应者就实现了方法的调用,执行了我们自定义的方法。

扩大点击范围

扩大点击范围用到了两个主要方法:

// 返回矩形是否包含指定的点
// rect检查矩形
// point 检查的点
CG_EXTERN bool CGRectContainsPoint(CGRect rect, CGPoint point)
    CG_AVAILABLE_STARTING(10.0, 2.0);


// 返回一个比源矩形小或大且具有相同中心点的矩形
// rect 原CGRect结构
// dx 用于调整源矩形x坐标值。若要缩小原矩形,请指定一个正值。若要扩大原矩形,请指定负值。
// dy 用于调整源矩形的y坐标值。若要缩小原矩形,请指定一个正值。若要扩大原矩形,请指定负值。
CG_EXTERN CGRect CGRectInset(CGRect rect, CGFloat dx, CGFloat dy) __attribute__ ((warn_unused_result))
    CG_AVAILABLE_STARTING(10.0, 2.0);

举例说明

创建一个UIButton子类,并重写其pointInside方法:

// 该方法返回YES,就会触发其响应事件
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // 将该button的原bounds大小扩大,x扩大20,y也扩大20
    CGRect bounds = CGRectInset(self.bounds, -20, -20);
    // 判断点击的点是否更改后的bouns中
    return CGRectContainsPoint(bounds, point);
}

或者我们重写hitTest方法,使其成为最佳响应者:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
	// 将该button的原bounds大小扩大,x扩大20,y也扩大20
    CGRect bounds = CGRectInset(self.bounds, -20, -20);
    // 判断点击的点是否更改后的bouns中
    if (CGRectContainsPoint(bounds, point)) {
        return self;
    } else {
        return nil;
    }
}

这里我们扩大其响应范围创建button大小是不会变化的,变化的只是我们看不到的其可以响应的范围。

点击穿透事件

点击穿透事件就比较麻烦了,它要在重写的UIButton中再传入一个你想要执行事件的button,通过它来响应点击事件。

举例说明

如图,视图1与视图2有重合部分3,当点击3时,我们希望视图1来响应这个点击事件

4234234
这时应该重写绿色部分hitTest的方法,同时还需要给绿色部分传入紫色部分的对象:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
	// 将紫色视图的响应范围转换出来,给point1
    CGPoint point1 = [self convertPoint:point toView:_purpleView];
    // 判断点击范围是否在紫色视图范围中,如果是就通过紫色视图来执行事件
    if ([_purpleView pointInside:point1 withEvent:event]) {
        return _purpleView;
    } else { // 否则就执行父类的方法
        return [super hitTest:point withEvent:event];
    }
}

iOS事件传递及响应者链条
参考
iOS触摸事件全家桶

原文地址:https://blog.csdn.net/m0_55124878/article/details/126185816

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_24770.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注