您的位置:首页 > 移动开发 > IOS开发

iOS事件传递与响应者链

2016-10-12 20:03 176 查看
从本质上讲,苹果设备响应事件的整个过程可以分为两个步骤:

步骤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:(<
c7fe
span class="hljs-built_in">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


最后

文章大部分内容参照 编程小翁 所写,觉得这个写得比较好;应用案例是本人项目所用到的,如有不懂或需要源码请留言。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ios