iOS事件传递与响应者链

从本质上讲,苹果设备响应事件的整个过程可以分为两个步骤:

步骤1:寻找目标。在iOS视图层次结构中找到触摸事件的最终接受者;
步骤2:事件响应。基于iOS响应者链(Responder Chain)处理触摸事件。

寻找目标

寻找目标是通过UIView的以下两个方法:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;//这个方法返回目标view
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event; //这个方法判断触摸点是否在当前view范围内

寻找目标的过程也称为hit-Testing,整个过程可用下图表示:
这里写图片描述

下面解释一下处理原理:

1、手指触摸屏幕,这个动作被包装成一个UIEvent对象发送给当前活跃的UIApplication (Active Application),Application将该Event对象插到任务队列的末尾等待处理(先进先出,先来的先处理);

2、UIApplication单例将事件发送给APP的主Window;

3、主Window调用视图层次结构上逐级使用hit-Testing确认最终的响应目标,这个目标也称为hitTesting view。

在没有做任何重载操作的前提下,系统默认的hit-Testing的处理机制如下:

 1.当前view调用自身的pointInside: withEvent:方法判断触摸点是否在自己范围内;

 2. 若pointInside: withEvent:方法返回NO,则说明触摸点不在自己范围内,则当前view的hitTest: withEvent:方法返回nil,当前view上的所有subview都不做判断。

 3.若pointInside: withEvent:方法返回YES,则说明触摸点在自己的范围内。但无法判断是否在自己身上还是在subview的身上。此时,遍历所有的subviews,对每个subview调用hitTest方法。这里要注意,遍历的顺序**是从当前view的subviews数组的尾部开始遍历**。因此离用户最近的上层的subview会优先被调用hitTest方法。

 4.一旦hitTest方法返回非空的view,则被返回的view就是最终相应触摸事件的view,寻找hitTesting view的阶段到此结束,不再遍历。

 5.若当前view的所有subviews的hitTest方法都返回nil,则当前view的hitTest方法返回self作为最终的hitTesting view,处理结束。

以上就是第一阶段寻找响应view的机制。

需要注意的几点:

1、hitTest方法调用pointInside方法;

2、hit-Testing过程是从superView向subView逐级传递,也就是从层次树的根节点向叶子节点传递;

3、遇到以下设置时,view的pointInside将返回NO,hitTest方法返回nil:

  • view.isHidden=YES;
  • view.alpah<=0.01;
  • view.userInterfaceEnable=NO;
  • control.enable=NO;(UIControl的属性)
  • 事件响应

    通过hit-Testing机制找到了hitTesting View之后,下面就是进行事件响应了。这个hitTesting View就是触摸事件的响应者Responder。在iOS系统中,能够响应并处理事件的对象称之为Responder Object,而UIResponder是所有responder的最顶层基类。当hitTesting view做完自己该做的动作后,可以根据需要将消息传给下一级响应者。那下一级响应者会是什么呢?这取决于iOS中的响应者链Responder Chain,如下图所示:

    这里写图片描述

    • UIView的nextResponder属性,如果有管理此view的UIViewController对象,则为此UIViewController对象;否则nextResponder即为其superview。
    • UIViewController的nextResponder属性为其管理view的superview.
    • UIWindow的nextResponder属性为UIApplication对象。
    • UIApplication的nextResponder属性为nil。

    更具体的:

    1.如果hit-test view或first responder不处理此事件,则将事件传递给其nextResponder处理,若有UIViewController对象则传递给UIViewController,否则传递给其superView。

    2.如果view的viewController也不处理事件,则viewController将事件传递给其管理view的superView。

    3.视图层级结构的顶级为UIWindow对象,如果window仍不处理此事件,传递给UIApplication.

    4.若UIApplication对象不处理此事件,则事件被丢弃。

    实际用途:

    响应者链在开发中可以解决那些问题呢?下面我就列举几个我在开发中遇到的问题:

    1. 通过view取出view所在的视图控制器ViewController(这段代码我是写在view的category里的):
      这里写图片描述
      当然了,你也可以通过响应者链取得其它类,不限于ViewController。

    2. 超出父视图的按钮响应事件。一般情况下我们的view是不会超出父视图的,但是在有些特殊的情况下,为了封装与坐标计算方便,子视图会超出。比如高德地图的“气泡”,在我们自定义气泡,你会发现上面添加的按钮是无法点击的,因为高德地图的气泡是跟“大头针”相关联的,所以弹出的气泡添加在了大头针上,大头针的大小是固定的,而且比起泡小得多。这总情况下就要重写气泡CustomAnnotationView的hitTest: withEvent:方法了:

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        UIView *view = [super hitTest:point withEvent:event];
        if (view == nil) {
            point = [self.calloutView.navBtn convertPoint:point fromView:self];
            if ([self.calloutView.navBtn.bounds pointInside:point withEvent:event])
            {
                view = self.calloutView.navBtn;
            }
        }
        return view;
    }

    这里的self.calloutView.navBtn 就是你需要点击的按钮。

    在开发项目时,也遇到了类似地图大头针这样的需求,如下图:
    这里写图片描述

    图上的标签位置是后台返回的,坐标以小红点为参考系,标签的文字长度是随文字变化的,点击标签进入相应的详情页。像这样的需求,首先我们可以看到这个标签是一个整体在许多地方都有用到,所以需要对它进行封装。具体代码如下:

    #import <UIKit/UIKit.h>
    
    typedef NS_ENUM(NSInteger, YFTagsViewType) {
        YFTagsViewTypeLeft = 0, //左侧
        YFTagsViewTypeRight = 1 //右侧
    };
    
    @interface YFTagsView : UIView
    
    @property (nonatomic, copy) NSString *title;    //标题
    @property (nonatomic, strong) UIImage *ico;     //图标
    @property (nonatomic, assign) BOOL isImage;     //是否显示图标
    @property (nonatomic, assign) CGFloat minWidth; //最小宽度
    @property (nonatomic, assign) CGFloat maxWidth; //最大宽度
    @property (nonatomic, assign) YFTagsViewType type;
    /*
     * 创建方法:
     * frame位置大小(YFTagsViewTypeLeft:右侧固定,宽度随变化。YFTagsViewTypeRight:相反)
     * type:类型
     */
    - (instancetype)initWithFrame:(CGRect)frame Type:(YFTagsViewType)type;
    
    @end
    #import "YFTagsView.h"
    #import "UIView+Animation.h"
    
    #define SNN ([UIScreen mainScreen].bounds.size.width)/(375)
    #define kZoom6pt(pt) ((pt)*(SNN))
    
    #define KArrorWeight kZoom6pt(15)
    #define space 0.70
    
    @implementation YFTagsView {
        UILabel *titleLabel;
        UIImageView *imgView;
        UIView *view;
        NSArray *layers;
    }
    
    - (void)drawRect:(CGRect)rect {
        BOOL isbool = YES;
        if (_type == YFTagsViewTypeLeft) {
            isbool = NO;
        }
    
        CGRect rrect = self.bounds;
        CGFloat radius = kZoom6pt(6),
        arrorRadius = kZoom6pt(3),
        arrorWeight = isbool?CGRectGetHeight(rrect)*space:-CGRectGetHeight(rrect)*space,
    
        minx = isbool?CGRectGetMinX(rrect) + kZoom6pt(10):CGRectGetMaxX(rrect) - kZoom6pt(10),
        maxx = isbool?CGRectGetMaxX(rrect):CGRectGetMinX(rrect),
    
        miny = CGRectGetMinY(rrect),
        midy = CGRectGetMidY(rrect),
        maxy = CGRectGetMaxY(rrect);
    
        UIBezierPath *path = [UIBezierPath bezierPath];
        [path moveToPoint:CGPointMake(minx, midy)];
    
        [path addCurveToPoint:CGPointMake(minx + arrorWeight, miny)
                controlPoint1:CGPointMake(minx, midy - arrorRadius)
                controlPoint2:CGPointMake(minx + arrorWeight - (isbool?arrorRadius:-arrorRadius), miny)];
    
        [path addLineToPoint:CGPointMake(maxx - (isbool?radius:-radius), miny)];
        [path addQuadCurveToPoint:CGPointMake(maxx, miny + radius) controlPoint:CGPointMake(maxx, miny)];
    
        [path addLineToPoint:CGPointMake(maxx, maxy - radius)];
        [path addQuadCurveToPoint:CGPointMake(maxx - (isbool?radius:-radius), maxy) controlPoint:CGPointMake(maxx, maxy)];
    
        [path addLineToPoint:CGPointMake(minx + arrorWeight, maxy)];
        [path addCurveToPoint:CGPointMake(minx, midy)
                controlPoint1:CGPointMake(minx + arrorWeight - (isbool?arrorRadius:-arrorRadius), maxy)
                controlPoint2:CGPointMake(minx, midy + arrorRadius)];
    
        [path closePath];
        UIColor *fillColor = [UIColor colorWithWhite:0.0 alpha:0.65];
        [fillColor set];
        [path fill];
    }
    
    - (instancetype)initWithFrame:(CGRect)frame Type:(YFTagsViewType)type {
        self = [super initWithFrame:frame];
        if (self) {
            _type = type;
            _minWidth = CGRectGetHeight(self.frame)*space + kZoom6pt(32);
            _maxWidth = [UIScreen mainScreen].bounds.size.width;
            self.backgroundColor = [UIColor clearColor];
            [self initSubViews];
        }
        return self;
    }
    
    - (void)initSubViews {
        imgView = [[UIImageView alloc] init];
        imgView.contentMode = UIViewContentModeScaleAspectFill;
        imgView.clipsToBounds = YES;
        imgView.image = [UIImage imageNamed:@"shoping"];;
        imgView.hidden = !_isImage;
        [self addSubview:imgView];
    
        titleLabel = [[UILabel alloc] init];
        titleLabel.font = [UIFont systemFontOfSize:kZoom6pt(12)];
        titleLabel.textColor = [UIColor whiteColor];
        titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
        titleLabel.textAlignment = _type == YFTagsViewTypeLeft?NSTextAlignmentLeft:NSTextAlignmentRight;
        [self addSubview:titleLabel];
    
        view = [[UIView alloc] init];
        view.backgroundColor = [UIColor colorWithWhite:1 alpha:0.75];
        view.layer.cornerRadius = kZoom6pt(5);
        view.hidden = _isImage;
        [self addSubview:view];
    
        CALayer *layer1 = [CALayer layer];
        layer1.frame = CGRectMake(0, 0, kZoom6pt(10), kZoom6pt(10));
        layer1.cornerRadius = kZoom6pt(5);
        layer1.backgroundColor = [UIColor colorWithWhite:1 alpha:0.75].CGColor;
        [view.layer addSublayer:layer1];
    
        CALayer *layer2 = [CALayer layer];
        layer2.frame = CGRectMake(0, 0, kZoom6pt(10), kZoom6pt(10));
        layer2.cornerRadius = kZoom6pt(5);
        layer2.backgroundColor = [UIColor colorWithWhite:1 alpha:0.75].CGColor;
        [view.layer addSublayer:layer2];
        layers = @[layer1, layer2];
    
        UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(kZoom6pt(2.5), kZoom6pt(2.5), kZoom6pt(5), kZoom6pt(5))];
        view1.backgroundColor = [UIColor colorWithRed:255/255.0 green:63/255.0 blue:139/255.0 alpha:255/255.0];
        view1.layer.cornerRadius = kZoom6pt(2.5);
        [view addSubview:view1];
    
        [self loadFrame];
    }
    
    - (void)loadFrame {
        titleLabel.textAlignment = _type == YFTagsViewTypeLeft?NSTextAlignmentLeft:NSTextAlignmentRight;
        CGFloat width = 0;
        NSString *str = titleLabel.text;
        //限制最多7个字
        for (int i = 7; i > 0; i--) {
            if (i <= str.length) {
                NSString *subStr = [str substringToIndex:i];
                width = [NSString widthWithString:subStr
                                             font:[UIFont systemFontOfSize:kZoom6pt(12)]
                              constrainedToHeight:kZoom6pt(18)] + CGRectGetHeight(self.frame)*space;
                width += _isImage?kZoom6pt(40):kZoom6pt(16);
    
                //宽度在最大与最小之间跳出循环
                if (width >= _minWidth && width <= _maxWidth) {
                    break;
                }
            }
        }
    
        if (width <  _minWidth) {
            width = _minWidth;
        }
    
    
        CGRect frame = self.frame;
        CGFloat x = (width - frame.size.width);
        frame.size.width = width;
    
        if (_type == YFTagsViewTypeRight) {
            view.frame = CGRectMake(0, (frame.size.height - kZoom6pt(10))/2, kZoom6pt(10), kZoom6pt(10));
            imgView.frame = CGRectMake(frame.size.width - kZoom6pt(24), kZoom6pt(3), kZoom6pt(18), kZoom6pt(18));
            if (_isImage) {
                titleLabel.frame = CGRectMake(kZoom6pt(10) + CGRectGetHeight(self.frame)*space, kZoom6pt(3), frame.size.width - kZoom6pt(40) - CGRectGetHeight(self.frame)*space, kZoom6pt(18));
            } else {
                titleLabel.frame = CGRectMake(kZoom6pt(10) + CGRectGetHeight(self.frame)*space, kZoom6pt(3), frame.size.width - kZoom6pt(16) - CGRectGetHeight(self.frame)*space, kZoom6pt(18));
            }
            self.frame = frame;
        } else if(_type == YFTagsViewTypeLeft){
            frame.origin.x -= x;
            view.frame = CGRectMake(frame.size.width - kZoom6pt(10), (frame.size.height - kZoom6pt(10))/2, kZoom6pt(10), kZoom6pt(10));
            imgView.frame = CGRectMake(kZoom6pt(6), kZoom6pt(3), kZoom6pt(18), kZoom6pt(18));
            if (_isImage) {
                titleLabel.frame = CGRectMake(kZoom6pt(30), kZoom6pt(3), frame.size.width - kZoom6pt(40) - CGRectGetHeight(self.frame)*space, kZoom6pt(18));
            } else {
                titleLabel.frame = CGRectMake(kZoom6pt(6), kZoom6pt(3), frame.size.width - kZoom6pt(16) - CGRectGetHeight(self.frame)*space, kZoom6pt(18));
            }
            self.frame = frame;
        }
        [self setNeedsDisplay];
    
        //动画
        [view scaleStatus:YES layers:layers];
    }
    
    
    #pragma mark - setter and getter
    
    - (void)setTitle:(NSString *)title {
        if ([title isEqual:[NSNull null]]) {
            title = @"";
        }
        NSArray *nameAry = [title componentsSeparatedByString:@"】"];
        NSString *name = [nameAry lastObject];
        _title = name?:@"";
        titleLabel.text = _title;
        [self loadFrame];
    }
    
    - (void)setIco:(UIImage *)ico {
        _ico = ico;
        imgView.image = _ico;
    }
    
    - (void)setIsImage:(BOOL)isImage {
        _isImage = isImage;
        imgView.hidden = !_isImage;
        [self loadFrame];
    }
    
    - (void)setMinWidth:(CGFloat)minWidth {
        if (minWidth > CGRectGetHeight(self.frame)*space + kZoom6pt(40)) {
            _minWidth = minWidth;
        } else {
            _minWidth = CGRectGetHeight(self.frame)*space + kZoom6pt(40);
        }
    
        if (minWidth > [UIScreen mainScreen].bounds.size.width) {
            _minWidth = [UIScreen mainScreen].bounds.size.width;
        }
    }
    
    - (void)setMaxWidth:(CGFloat)maxWidth {
        if (maxWidth < [UIScreen mainScreen].bounds.size.width) {
            _maxWidth = maxWidth;
        } else {
            _maxWidth = [UIScreen mainScreen].bounds.size.width;
        }
    
        if (maxWidth < CGRectGetHeight(self.frame)*space + kZoom6pt(40)) {
            _maxWidth = CGRectGetHeight(self.frame)*space + kZoom6pt(40);
        }
    }
    
    @end
    
    #import <UIKit/UIKit.h>
    #import "YFTagsView.h"
    
    @interface YFTagButton : UIControl
    
    // 创建方法
    - (instancetype)initWithType:(YFTagsViewType)type;
    + (instancetype)buttonWithType:(YFTagsViewType)type;
    
    /*
     @brief 赋值
     @param title:   标题
     @param price:   价格
     @param origin:  坐标(动画小圆点的中心)
     @param isImg:   是否显示购物车图标及价格标签
     @param type:    类型
     */
    - (void)setTitle:(NSString *)title price:(NSString *)price origin:(CGPoint)origin isImg:(BOOL)isImg type:(YFTagsViewType)type;
    
    @end
    #import "YFTagButton.h"
    #import "GlobalTool.h"
    
    #define scax 1
    
    @implementation YFTagButton {
        UIImageView *imgView;
        UILabel *priceLabel;
        YFTagsView *tagView;
        CGPoint _origin;
        NSString *_title;
        NSString *_price;
        BOOL _isImg;
        YFTagsViewType _type;
    }
    
    + (instancetype)buttonWithType:(YFTagsViewType)type {
        YFTagButton *btn = [[YFTagButton alloc] initWithType:type];
        return btn;
    }
    
    - (instancetype)initWithType:(YFTagsViewType)type {
        self = [super init];
        if (self) {
            _type = type;
            [self setUI];
        }
        return self;
    }
    
    - (instancetype)init {
        if (self = [super init]) {
            [self setUI];
        }
        return self;
    }
    
    - (void)setUI {
        imgView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"price_ tag"]];
        imgView.frame = CGRectMake(kZoom6pt(-16), kZoom6pt(-5), kZoom6pt(32), kZoom6pt(43));
        [self addSubview:imgView];
        priceLabel = [[UILabel alloc] init];
        priceLabel.textColor = [UIColor whiteColor];
        priceLabel.textAlignment = NSTextAlignmentCenter;
        priceLabel.font = [UIFont systemFontOfSize:kZoom6pt(9)];
        priceLabel.frame = CGRectMake(0, imgView.height - kZoom6pt(20), kZoom6pt(32), kZoom6pt(20));
        [imgView addSubview:priceLabel];
        tagView = [[YFTagsView alloc] initWithFrame:CGRectMake(_type == YFTagsViewTypeRight?kZoom6pt(-5):kZoom6pt(-20), -kZoom6pt(12), kZoom6pt(25), kZoom6pt(24)) Type:_type];
        tagView.userInteractionEnabled = NO;
        [self addSubview:tagView];
    }
    
    - (void)setTitle:(NSString *)title price:(NSString *)price origin:(CGPoint)origin isImg:(BOOL)isImg type:(YFTagsViewType)type{
        _type = type;
        _origin = origin;
        _title = title?:@"";
        _price = price?:@"";
        _isImg = isImg;
        if (self.superview == nil) {
            return;
        }
        tagView.frame = CGRectMake(_type == YFTagsViewTypeRight?kZoom6pt(-5):kZoom6pt(-20), -kZoom6pt(12), kZoom6pt(25), kZoom6pt(24));
        CGPoint point = _type == YFTagsViewTypeLeft?CGPointMake(kZoom6pt(168),kZoom6pt(110)):CGPointMake(kZoom6pt(204),kZoom6pt(237));
        if (origin.x||origin.y) {
            CGFloat h = (scax*self.superview.width - self.superview.height)/2;
            point = CGPointMake(self.superview.width*origin.x, origin.y*self.superview.width*scax - h);
        }
        self.origin = point;
        CGFloat maxWidth = _type == YFTagsViewTypeLeft?point.x + kZoom6pt(5):self.superview.width - point.x + kZoom6pt(5);
        tagView.type = type;
        tagView.maxWidth = maxWidth;
        tagView.isImage = isImg;
        tagView.title = title?:@"";
        priceLabel.text = price?:@"";
        imgView.hidden = !isImg;
        self.userInteractionEnabled = isImg;
    }
    
    - (void)didMoveToSuperview {
        [super didMoveToSuperview];
        [self setTitle:_title price:_price origin:_origin isImg:_isImg type:_type];
    }
    
    - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
        CGPoint tagPoint =  [touch locationInView:tagView];
        CGPoint imgPoint =  [touch locationInView:imgView];
        if ([tagView pointInside:tagPoint withEvent:event]||[imgView pointInside:imgPoint withEvent:event]) {
            [self sendActionsForControlEvents:UIControlEventTouchUpInside];
        }
        return NO;
    }
    
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
        UIView *view = [super hitTest:point withEvent:event];
        if (view == nil&&self.userInteractionEnabled) {
            CGPoint tagPoint = [tagView convertPoint:point fromView:self];
            CGPoint imgPoint = [imgView convertPoint:point fromView:self];
            if ([tagView pointInside:tagPoint withEvent:event]||[imgView pointInside:imgPoint withEvent:event]) {
                view = self;
            }
        }
        return view;
    }
    
    @end
    

    最后

    文章大部分内容参照 编程小翁 所写,觉得这个写得比较好;应用案例是本人项目所用到的,如有不懂或需要源码请留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值