美文网首页
2020-10-13

2020-10-13

作者: 张_何 | 来源:发表于2020-10-13 13:07 被阅读0次

官方文档

这里先给好用到的源码

#import "ResponseChainVC.h"
@interface ResponseBtnA: UIButton
@end
@implementation ResponseBtnA
@end
@interface ResponseBtnB: UIButton
@end
@implementation ResponseBtnB
@end

@interface ResponseBaseView: UIView
@property(copy,nonatomic) NSString *name;
@property(strong,nonatomic) UILabel *nameLabel;
@end

@implementation ResponseBaseView
-(UILabel *)nameLabel{
    if (!_nameLabel) {
        _nameLabel = [[UILabel alloc] init];
        _nameLabel.frame = CGRectMake(0, 0, 30, 20);
        _nameLabel.textColor = UIColor.blackColor;
        _nameLabel.font = [UIFont systemFontOfSize:20];
        [_nameLabel sizeToFit];
        _nameLabel.backgroundColor = UIColor.whiteColor;
    }
    return _nameLabel;
}
-(void)setName:(NSString *)name{
    _name = name;
    self.nameLabel.text = name;
    [self.nameLabel sizeToFit];
}
-(instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        [self addSubview:self.nameLabel];
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
        [self addGestureRecognizer:tap];
    }
    return self;
}
-(void)tapAction:(UITapGestureRecognizer *)sender{
    NSLog(@"%@ tapAction",NSStringFromClass([self class]));
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    NSLog(@"%@ touchesBegan",NSStringFromClass([self class]));
}
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *v = [super hitTest:point withEvent:event];
    NSLog(@"%@ hitTest %@",NSStringFromClass([self class]),v);
    return  v;
}
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
    NSLog(@"%@ pointInside",NSStringFromClass([self class]));
    return CGRectContainsPoint(self.bounds, point);
}
@end

@interface ResponseViewA: ResponseBaseView
@end
@implementation ResponseViewA
@end

@interface ResponseViewB: ResponseBaseView
@end

@implementation ResponseViewB
@end

@interface ResponseViewC: ResponseBaseView
@end

@implementation ResponseViewC
@end

@interface ResponseViewD: ResponseBaseView
@end

@implementation ResponseViewD
@end

@interface ResponseViewE: ResponseBaseView
@end

@implementation ResponseViewE
@end

@interface ResponseViewF: ResponseBaseView
@end

@implementation ResponseViewF
@end

@interface ResponseChainVC ()
@end

@implementation ResponseChainVC
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.whiteColor;
//    [self testBtn];
    [self testView];
}
-(void)testView{
    ResponseViewA *a = [[ResponseViewA alloc] initWithFrame:CGRectMake(20, 100, 300, 250)];
    a.backgroundColor = UIColor.redColor;
    a.name = @"A";
    [self.view addSubview:a];

    ResponseViewB *b = [[ResponseViewB alloc] initWithFrame:CGRectMake(20, 100, 100, 100)];
    b.backgroundColor = UIColor.yellowColor;
    b.name = @"B";
    [a addSubview:b];

    ResponseViewC *c = [[ResponseViewC alloc] initWithFrame:CGRectMake(25, 270, 300, 250)];
    c.backgroundColor = UIColor.blueColor;
    c.name = @"C";
    [self.view addSubview:c];
    
    ResponseViewD *d = [[ResponseViewD alloc] initWithFrame:CGRectMake(30, -30, 100, 100)];
    d.backgroundColor = UIColor.greenColor;
    d.name = @"D";
    [c addSubview:d];
    
    ResponseViewE *e = [[ResponseViewE alloc] initWithFrame:CGRectMake(20, -30, 100, 100)];
    e.backgroundColor = UIColor.grayColor;
    e.name = @"E";
    [d addSubview:e];
    
    ResponseViewF *f = [[ResponseViewF alloc] initWithFrame:CGRectMake(20, 100, 100, 100)];
    f.backgroundColor = UIColor.purpleColor;
    f.name = @"F";
    [c addSubview:f];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
//    NSLog(@"ResponseChainVC touchesBegan");
//    NSLog(@"class = %@, nextResponder = %@",[self class],self.nextResponder);
//    NSLog(@"class = %@, nextResponder = %@",[self.view class],self.view.nextResponder);
//    for (int i = 0; i < self.view.subviews.count; i ++){
//        UIView *v = self.view.subviews[i];
//        NSLog(@" class = %@, nextResponder = %@",[v class],v.nextResponder);
//    }
   
    UIResponder *r = self.view.nextResponder;
    while (r) {
        NSLog(@"class = %@, nextResponder = %@",[r class],r);
        r = r.nextResponder;
    }
}

-(void)testBtn{
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(20, 100, 100, 100);
    btn.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn];
    
//    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction:)];
//    [btn addGestureRecognizer:tap];
    
    [btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
}

-(void)btnAction:(UIButton *)sender{
    UIResponder *nextResponder = sender.nextResponder;
    while (nextResponder) {
        NSLog(@"btnAction nextResponder = %@",nextResponder);
        nextResponder = nextResponder.nextResponder;
    }
}

-(void)tapAction:(UITapGestureRecognizer *)sender{
    UIResponder *nextResponder = sender.view.nextResponder;
    while (nextResponder) {
       NSLog(@"tapAction nextResponder = %@",nextResponder);
       nextResponder = nextResponder.nextResponder;
    }
}

@end

// 此类只是用来测试push 出的vc和 present出来的vc中nextResponder的区别,其他情况都在ResponseChainVC中测试
@interface ResponseChainVCA: UIViewController
@end

@implementation ResponseChainVCA
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.whiteColor;
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(20, 100, 100, 100);
    btn.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn];
     [btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
}

-(void)btnAction:(UIButton *)sender{
    Class c = NSClassFromString(@"ResponseChainVC");
    UIViewController *vc = [[c alloc] init];
    vc.modalPresentationStyle = UIModalPresentationFullScreen;
    [self presentViewController:vc animated:true completion:false];
//    [self.navigationController pushViewController:vc animated:true];
}


@end

事件

  • 对于 iOS 设备用户来说,他们操作设备的方式主要有三种:触摸屏幕、晃动设备、通过遥控设施控制设备。对应的事件类型有以下三种:

1、触屏事件(Touch Event)
2、运动事件(Motion Event)
3、远端控制事件(Remote-Control Event)

  • iOS 中只有继承自UIResponder的类才能处理事件
    @interface UIApplication : UIResponder
    @interface UIView : UIResponder
    @interface UIViewController : UIResponder

本文只针对触屏事件做介绍

事件的产生

  • iOS 系统检测到手指触摸操作时会将其打包成一个 UIEvent 对象,并放入当前活动 Application 的事件队列(先进先出),单例的 UIApplication 会从事件队列中取出最前面的事件交给keyWindow处理。那么keyWindow怎么知道将事件分发给谁呢?它是怎么确定事件由谁来响应呢?

响应者

  • 响应者对象是能处理事件的对象,也就是继承自UIResponder的对象,像UIView、UIViewController、UIApplication。响应链就是由响应者通过nextResponder链接起来的。

查找具体响应者

ios 查找具体响应者的过程我们成为Hit-Test,那么什么是Hit-Test呢,我们可以把它理解为一个探测器,通过这个探测器我们可以找到并判断手指是否点击在某个视图上面,换句话说就是通过Hit-Test可以找到手指点击到的处于屏幕最前面的那个UIView。那么它是怎么找的呢?
首先我们看UIView的两个方法:

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

Hit-Test采用的是递归调用,从向根节点的UIWindow发送hitTest:withEvent:消息开始,这个消息返回一个UIView对象,当返回的UIView对象不为nil时结束,如果递归调用结束返回的是nil,则该事件被丢弃。返回的这个UIView对象就是找到的改事件的响应者。当向UIWindow发送hitTest:withEvent:消息时,hitTest:withEvent:里面所做的事,就是判断当前的点击位置是否在window里面,如果在则遍历window的subview然后依次对subview发送hitTest:withEvent:消息(注意这里给subview发送消息是根据当前subview的index顺序,index越大就越先被访问,也就是倒序的方式),一旦找到UIView对象,则递归结束。当然hitTest:withEvent:方法会先判读当前的view能否接收事件,UIView能接收事件的条件如下:
1、alpha > 0.01, 注意不包括等于,亲测
2、userInteractionEnabled属性为YES
3、hidden属性为NO
大致代码如下:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    if (self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.1) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
    }
    return nil;
}

派发

  • 通过Hit-Test找到最合适的响应者后,application通过sendEvent:方法将事件发送给UIWindow,然后UIWindow通过同样的方法,把事件发送给响应者。

我们对ResponseChainVC中的btnAction断点调试,点击button触发断点我们通过控制台查看函数调用栈可以验证
对btnAction方法断点调试查看执行该方法时调用的函数栈

//(lldb) bt
//* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
//  ObjcCode`-[ResponseChainVC btnAction:](self=0x00007f9b7e515010, _cmd="btnAction:", sender=0x00007f9b8121d360) at ResponseChainVC.m:106:34
//   UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 83
//   UIKitCore`-[UIControl sendAction:to:forEvent:] + 223
//   UIKitCore`-[UIControl _sendActionsForEvents:withEvent:] + 396
//   UIKitCore`-[UIControl touchesEnded:withEvent:] + 497
//   UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1359
//   UIKitCore`-[UIWindow sendEvent:] + 4501
//   UIKitCore`-[UIApplication sendEvent:] + 356
//   UIKitCore`__dispatchPreprocessedEventFromEventQueue + 7328
//   UIKitCore`__handleEventQueueInternal + 6565
//   UIKitCore`__handleHIDEventFetcherDrain + 88
//   CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
//   CoreFoundation`__CFRunLoopDoSource0 + 76
//   CoreFoundation`__CFRunLoopDoSources0 + 180
//   CoreFoundation`__CFRunLoopRun + 974
//   CoreFoundation`CFRunLoopRunSpecific + 404
//   GraphicsServices`GSEventRunModal + 139
//   UIKitCore`UIApplicationMain + 1605
//   ObjcCode`main(argc=1, argv=0x00007ffee93b2cb0) at main.m:14:16
//   libdyld.dylib`start + 1
  • 通过查看函数调用栈发现以下几点:
    1、事件源都是CFRunLoopDoSource0
    2、都经过UIApplication处理
    3、最终由响应者调用响应方法

响应链

  • 什么是响应链: app中,所有的视图都是按照一定的结构组织起来的,即树状层次结构,每个view都有自己的superView,包括controller的topmost view(controller的self.view)。当一个view被add到superView上的时候,他的nextResponder属性就会被指向它的superView,当controller被初始化的时候,self.view(topmost view)的nextResponder会被指向所在的controller,而controller的nextResponder会被指向self.view的superView,这样,整个app就通过nextResponder串成了一条链,也就是我们所说的响应链。所以响应链就是一条虚拟的链,并没有一个对象来专门存储这样的一条链,而是通过UIResponder的属性串连起来的
  • 响应链是由nextResponder连起来的一系列UIResponder对象构成的
  • 响应链是在什么时候确定的 :当view添加到superView的时候被确定。

下面通过例子阐述一下由nextResponder连起来的响应链

下面我们测试一下直接从导航控制器中push到ResponseChainVC的情况,
将ResponseChainVC的touchesBegan:withEvent:方法修改为

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [super touchesBegan:touches withEvent:event];
    UIResponder *r = self.view.nextResponder;
    while (r) {
        NSLog(@"class = %@, nextResponder = %@",[r class],r.nextResponder);
        r = r.nextResponder;
    }
}

进入到ResponseChainVC页面,点击空白地方查看touchesBegan:withEvent:执行打印的情况如下:

class = ResponseChainVC, nextResponder = <UIViewControllerWrapperView: 0x7fc0a830aa90>
class = UIViewControllerWrapperView, nextResponder = <UINavigationTransitionView: 0x7fc0a82064f0>
class = UINavigationTransitionView, nextResponder = <UILayoutContainerView: 0x7fc0a5c09290>
class = UILayoutContainerView, nextResponder = <UINavigationController: 0x7fc0a6010a00>
class = UINavigationController, nextResponder = <UIDropShadowView: 0x7fc0a5e04400>
class = UIDropShadowView, nextResponder = <UITransitionView: 0x7fc0a5d05780>
class = UITransitionView, nextResponder = <UIWindow: 0x7fc0a5f07a00>
class = UIWindow, nextResponder = <UIWindowScene: 0x7fc0a8004ad0>
class = UIWindowScene, nextResponder = <UIApplication: 0x7fc0a5c060c0>
class = UIApplication, nextResponder = <AppDelegate: 0x6000019d0a20>
class = AppDelegate, nextResponder = (null)

下面我们测试一下直接从导航控制器中push到ResponseChainVCA,然后通过点击ResponseChainVCA中的button present到ResponseChainVC的情况
这里将ResponseChainVCA中的btnAction:方法修改为

-(void)btnAction:(UIButton *)sender{
    Class c = NSClassFromString(@"ResponseChainVC");
    UIViewController *vc = [[c alloc] init];
    vc.modalPresentationStyle = UIModalPresentationFullScreen;
    [self presentViewController:vc animated:true completion:false];
}

进入到ResponseChainVC页面,点击空白地方查看touchesBegan:withEvent:执行打印的情况如下:

class = ResponseChainVC, nextResponder = <UINavigationController: 0x7fbd41808200>
class = UINavigationController, nextResponder = <UITransitionView: 0x7fbd4142d0c0>
class = UITransitionView, nextResponder = <UIWindow: 0x7fbd4160d5b0>
class = UIWindow, nextResponder = <UIWindowScene: 0x7fbd41509190>
class = UIWindowScene, nextResponder = <UIApplication: 0x7fbd43804f10>
class = UIApplication, nextResponder = <AppDelegate: 0x600003c04f00>
class = AppDelegate, nextResponder = (null)

通过对比我们发现
1、ResponseChainVC.view 的nextResponder 是ResponseChainVC
2、不论是push还是present ResponseChainVC响应链的最终都是

class = UIWindow, nextResponder = <UIWindowScene>
class = UIWindowScene, nextResponder = <UIApplication>
class = UIApplication, nextResponder = <AppDelegate>
class = AppDelegate, nextResponder = (null)

其实对nextResponder有如下规则:
1、如果 view 是一个 view controller 的 root view,nextResponder 是这个 view controller.
2、如果 view 不是 view controller 的 root view,nextResponder 则是这个 view 的 superview
3、如果 view controller 的 view 是 window 的 root view, view controller 的 nextResponder 是这个 window
4、如果 view controller 是被其他 view controller presented调起来的,那么 view controller 的 nextResponder 就是发起调起的那个 view controller
5、window 的 nextResponder 是 UIApplication 对象. ios 11 之后新增了UIWindowScene,window的nextResponder变成了UIWindowScene,UIWindowScene的nextResponder是UIApplication对象
6、UIApplication 对象的 nextResponder 是 app delegate, 但是 app delegate 必须是 UIResponder 对象


事件拦截应用场景

扩大UIView事件响应区域
  • 给UIView扩大响应区域,主要是在pointInside:withEvent:方法中判断点击点是否包含在扩大后的区域里,如果包含就返回true。这里需要注意点击的区域要在父视图内,如果不在父视图内根本不会通过Hit-Test,所以压根也不会调用pointInside:withEvent:方法。
    下面举例给UIButton扩大响应区域pointInside:withEvent:
@interface UIButton (EnlargeEdge)
// 用 IBInspectable 修饰该属性后,该属性会在XIB中显示,可以在xib中设置
@property(nonatomic) IBInspectable CGFloat enlargeEdge;

-(void)setEnlargeWithTop:(CGFloat)top left:(CGFloat)left bottom:(CGFloat)bottom right:(CGFloat)right;
@end

#import "UIButton+EnlargeEdge.h"
#import <objc/runtime.h>

static char topKey;
static char bottomKey;
static char leftKey;
static char rightKey;

@implementation UIButton (EnlargeEdge)


-(void)setEnlargeEdge:(CGFloat)enlargeEdge {
    [self setEnlargeWithTop:enlargeEdge left:enlargeEdge bottom:enlargeEdge right:enlargeEdge];
}

-(CGFloat)enlargeEdge {
    return [objc_getAssociatedObject(self, &topKey) floatValue];
}

-(void)setEnlargeWithTop:(CGFloat)top left:(CGFloat)left bottom:(CGFloat)bottom right:(CGFloat)right {
    objc_setAssociatedObject(self, &topKey,   [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &bottomKey,   [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &leftKey,   [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &rightKey,   [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

-(CGRect)enlargedRect {
    NSNumber* topEdge    = objc_getAssociatedObject(self, &topKey);
    NSNumber* rightEdge  = objc_getAssociatedObject(self, &rightKey);
    NSNumber* bottomEdge = objc_getAssociatedObject(self, &bottomKey);
    NSNumber* leftEdge   = objc_getAssociatedObject(self, &leftKey);

    if (topEdge && rightEdge && bottomEdge && leftEdge) {
        return CGRectMake(self.bounds.origin.x    - leftEdge.floatValue,
                          self.bounds.origin.y    - topEdge.floatValue,
                          self.bounds.size.width  + leftEdge.floatValue + rightEdge.floatValue,
                          self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
    } else {
        return self.bounds;
    }
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect rect = [self enlargedRect];
    if (CGRectEqualToRect(rect, self.bounds)) {
        return [super pointInside:point withEvent:event];
    }
    return CGRectContainsPoint(rect, point) ? YES : NO;
}

@end
多级响应
  • 这种通常是事件穿透效果,比如下图中常见的直播间礼物面板,它的下半部分是供用户选择的礼物,而上半部分是透明的,通常的交互是当礼物面板弹出来以后点击透明区域让礼物面板消失,但也有时候产品会要求用户点击透明区域的时候既让面板消失也让让蓝色框中的按钮响应事件,比如点击当礼物面板出来后点击排行榜,既让礼物面板消失,同时又弹出排行榜。
    这个时候就可以在礼物面板中的hittest方法中处理了,在hittest方法中判断点击的区域如果是透明区,则让礼物面板消失,同时返回nil,这样事件就会继续向下传递。


    image.png

注意点

  • 当一个UIControl或其子类同时添加了点击事件和手势事件的时候,会优先响应手势,因为手势事件的优先级比较高会响应手势。对一个button同时添加action和tap手势,会执行tap手势的响应方法。

参考
参考
参考
参考
参考

相关文章

网友评论

      本文标题:2020-10-13

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