从本质上讲,苹果设备响应事件的整个过程可以分为两个步骤:
步骤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对象不处理此事件,则事件被丢弃。
实际用途:
响应者链在开发中可以解决那些问题呢?下面我就列举几个我在开发中遇到的问题:
通过view取出view所在的视图控制器ViewController(这段代码我是写在view的category里的):
当然了,你也可以通过响应者链取得其它类,不限于ViewController。超出父视图的按钮响应事件。一般情况下我们的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
最后
文章大部分内容参照 编程小翁 所写,觉得这个写得比较好;应用案例是本人项目所用到的,如有不懂或需要源码请留言。