事件的传递和响应
- UIResponder和响应链的组成
许多对象都继承自UIResponder,包括UIApplication对象,UIViewController对象以及所有的UIView对象,也包括UIWindow对象。
UIResponder苹果官方文档
响应链
响应链的组成是:
- 如果你这个view的上一级是viewController那么他的nextResponder是viewController,但是如果上一级viewController有自己的view,则是他的nextResponder先是上一级viewController的view,然后才是viewController。
- 如果你这个view的上一级有superView,那么他的nextResponder是superView,如果到了最顶层是window,那么返回的就是window了,最后是UIApplication,再最后是AppDelegate。
注意:当加了导航条的时候,系统会默认加一些导航的view和导航控制器在里面,所以用nextResponder获取不到你想要的从导航push过来的上一个控制器。
- Hit-Test找到view的流程
当用户产生一个触摸事件,UIKit会生成一个event object包含了该事件需要处理的信息(时间、位置、状态等),然后把该event object放在app的事件队列里(先进先出),触摸事件它是一个NSSet型的touches。对于motion事件(加速计等),event取决于是哪种motion event。
事件沿着一个特定的路径来传递到可处理事件的对象上来,首先UIApplication从事件队列里拿到一个event进行分发,传给keyWindow,再传给initial object(取决于事件的type)。
- touch事件:keywindow传递事件给事件发生的view(通过hitTest方法来递归找到)
- motion和远程控制事件
当用户点击屏幕后,事件响应的流程
分析:当用户点击屏幕产生事件后,UIApplication从事件队列中给UIWindow分发一个事件,然后UIWindow遍历它的子视图,其中谁先添加,就先从谁哪里开始找,如果它有子视图就再遍历它的子视图,如果最终这个view的pointInside:withEvent:返回的是NO,就说明触摸点不在这个View上面。然后它就开始遍历它的兄弟view,也就是第二个被添加的view,如果有子视图也会遍历,如此下去,直到找到需要响应的view。如果最终都没有被响应,则事件则会被抛弃(反序遍历)。
影响Hit-Test流程的因素有:alpha、hidden、userInteractionEnabled
pointInside:withEvent:是在Hit-Test中进行调用的,当alpha<0.01,hidden = YES,或者userInteractionEnabled = NO,响应都只会走到Hit-Test方法,不会走到pointInside:withEvent:,也即是事件不会被响应。
hitTest: withEvent:内部实现原理:
if (self.alpha <= 0.01 || self.hidden || !self.userInteractionEnabled) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects]; // 交换顺序,后添加的先遍历
for (id view in subViews) {
CGPoint convertPoint = [self convertPoint:point toView:view]; // 将point to到view上
if ([view pointInside:convertPoint withEvent:event]) {
return view;
}
}
return self;
}
return nil;
第二种(两种都可以):
UIView *lastResultView = nil;
if ([self pointInside:point withEvent:event]) {
lastResultView = self;
NSArray *sub = [[self.subviews reverseObjectEnumerator] allObjects]; // 交换顺序,后添加的先遍历
if (sub.count) {
for (id view in sub) {
CGPoint convertPoint = [self convertPoint:point toView:view]; // 将point to到view上
UIView *currentResultView = [view hitTest:convertPoint withEvent:event];
if (currentResultView) {
lastResultView = currentResultView;
break;
}
}
return lastResultView;
} else {
return lastResultView;
}
}
return nil;
第三种:
if (self.alpha <= 0.01 || !self.userInteractionEnabled || self.hidden) {
return nil;
}
if ([self pointInside:point withEvent:event]) { //发生在我的范围内
//遍历子view reverseObjectEnumerator 反序 objectEnumerator 顺序
NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects];
UIView *tmpView;
for (UIView *subView in subViews) {
//转换坐标系,然后判断该点是否在bounds范围内
CGPoint convertedPoint = [self convertPoint:point toView:subView]; //将点的坐标系转换到以自控件为标准
tmpView = [subView eocHitTest:convertedPoint withEvent:event];
}
return tmpView?tmpView:self; // 如果存在子控件被点击就返回子控件,不然就返回自身
} else {
return nil;
}
应用
- 扩大button的响应范围
可以通过重写pointInside方法来实现
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect btnBounds = self.bounds;
//扩大点击区域
btnBounds = CGRectInset(btnBounds, -W(30), -W(30));
//如果点击的点在新的bounds里,就返回yes
return CGRectContainsPoint(btnBounds, point);
}
- 创建一个超出superView的控件,点击超出部分还能响应事件
做法一:可以重写父控件的pointInside方法,返回YES
做法二:可以重写父控件的hitTest: withEvent:方法,遍历它的子控件,去掉在父控件上面才遍历的那个判断
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.alpha <= 0.01 || self.hidden || !self.userInteractionEnabled) {
return nil;
}
// if ([self pointInside:point withEvent:event]) {
NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects]; // 交换顺序,后添加的先遍历
for (id view in subViews) {
CGPoint convertPoint = [self convertPoint:point toView:view]; // 将point to到view上
if ([view pointInside:convertPoint withEvent:event]) {
return view;
}
}
return self;
// }
事件响应的四个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
[super touchesCancelled:touches withEvent:event];
}
当只有调用了父类的方法[super touchesBegan:touches withEvent:event]才能将响应事件传递个下一个响应者,也可以写成[self.nextResponder touchesBegan:touches withEvent:event]。
手势
手势事件
手势把低层次的事件处理变成高层次的响应。需要把手势附加到view上,允许view响应定义好的action。当识别手势后将打断view的touch事件。当识别手势后,发送action消息给target,target是view controller。-
手势的状态变化
非连续性的手势状态变化:当识别后,手势状态从Possible到recognized转变,然后识别完成。
连续性的手势状态变化:手势识别后,手势从Possible到begin状态,然后由begin到changed状态,如果手势一直发生,再持续性为changed状态,当手指离开后手势变成end状态,手势完成,end状态是recognized状态的别名。
只要手势不是切换到failed或canceled状态,当手势状态改变的时候,都会发送action message给target。所以非连续性的手势只会发送一次action message,而连续性的会发送多次。
上图中左边为非连续性,右边为连续性
与其他手势之间的交互
如果一个view有多个手势,默认情况下是没有次序来说明哪个手势先识别的,所以每一次识别的次序是不一样的。所以可以用UIGestureRecognizeDelegate来处理先后顺序。-
手势和touch
手势是附加在控件上,可以在控件外部来响应,但是touch只能在控件内部来响应。
默认情况下,当一个touch发生的时候,touch object从UIApplication传递到UIWindow,window首先传递touches给绑定了手势产生的touch的view或superViews,然后再进行touchBegin四个方法来处理(如下图)。
window延迟传送touch objects给view,使得手势能先处理touch,在延迟的时候,如果手势识别出来了,window不会再传递给touch给view,也会取消,打断之前传递给view的touch(如下图)。
手势里面也有响应touch的是个方法,当导入#import <UIKit/UIGestureRecognizerSubclass.h>,即可重写这四个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"RedColorTapGesture touchBegan %ld", self.state);
[super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"RedColorTapGesture touchesMoved %ld", self.state);
[super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"RedColorTapGesture touchesEnded %ld", self.state);
[super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"RedColorTapGesture touchesCancelled %ld", self.state);
[super touchesCancelled:touches withEvent:event];
}
如果不写touchesBegan方法中的[super touchesBegan:touches withEvent:event],手势将不会被响应。
在一个控件上添加手势时,会先走到手势的touchesBegan方法,再走到控件的touchesBegan方法
影响view的touch方法手势的几个属性
-
delaysTouchesBegan 默认为NO,当我们手势识别之后,直接取消对touch事件的响应
当设置为YES时,也即是不延迟响应时,就不会识别view的touch方法,只是识别和处理手势的touch方法
-
cancelsTouchesInView 默认为YES,当手势识别后会打断view的touch事件
当设置为NO后,则是直接结束view的touch事件
delaysTouchesEnded 默认为YES,这种情况下发生一个touch时,在手势识别成功后,发送给touchesCancelled消息给hit-test,先打断,延迟结束。手势识别失败时,会延迟大概0.15ms,期间没有接收到别的touch才会发送touchesEnded。如果设置为NO,则不会延迟,即会立即发送touchesEnded以结束当前触摸。
多个手势共存问题
假如一个控件上有两个手势,比如
这时只会响应pan手势,不会响应swipe手势
- 当加上
[panGesture requireGestureRecognizerToFail:swipeGesture]
这句代码时,两个手势共存,从左往右滑会响应swipe手势。 - 也可以通过手势的代理方法来实现
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
NSLog(@"RedColorTapGesture RecognizerShouldBegin");
return YES;
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
//这里返回YES,代表跟别的手势共存;如果返回NO,不一定代表不共存,只要别的手势返回YES,也能共存
NSLog(@"RedColorTapGesture RecognizerSimultaneously");
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{ //另外一个手势识别fail的时候,才会识别自己
NSLog(@"RedColorTapGesture RequireFailure");
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
//我被另外一个手势变成Fail
NSLog(@"RedColorTapGesture shouldBeRequiredToFail");
return NO;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
NSLog(@"RedColorTapGesture shouldReceiveTouch");
return YES;
}
button事件
- 代码主动响应button的事件
当调用[customBtn sendActionsForControlEvents:UIControlEventTouchDown]
时,会自动执行button的点击事件。 - button的内部实现
在事件的点击事件打断点,看进程可以知道button内部调用了两个方法,所以可以推断它的内部实现:
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
return [super sendAction:action to:self forEvent:event];
}
- (void)btnAction
{
NSLog(@"八点钟学院");
}
button的UIControlEvents几种状态,都是在button的touch四个方法中进行区分判断的
可以继承自UIControl,自定义一个自己想要的button
@interface EOCButton : UIControl
- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title image:(UIImage *)image;
#import "EOCButton.h"
@interface EOCButton ()
{
NSString *_title;
UIImage *_image;
UILabel *_titleLabel;
UIImageView *_imageView;
}
@end
@implementation EOCButton
- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title image:(UIImage *)image
{
if (self = [super initWithFrame:frame]) {
_title = title;
_image = image;
[self craeteTitleLabel];
[self createImageView];
}
return self;
}
- (void)craeteTitleLabel
{
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont systemFontOfSize:12.f];
_titleLabel.text = _title;
_titleLabel.textColor = [UIColor redColor];
_titleLabel.textAlignment = NSTextAlignmentCenter;
[self addSubview:_titleLabel];
}
- (void)createImageView
{
_imageView = [[UIImageView alloc] init];
_imageView.image = _image;
[self addSubview:_imageView];
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat labelHeight = 20.f;
_titleLabel.frame = CGRectMake(0.f, self.eocH-labelHeight, self.eocW, labelHeight);
_imageView.frame = CGRectMake(0.f, 0.f, self.eocW, self.eocH);
}
使用上和button一样
EOCButton *btn = [[EOCButton alloc] initWithFrame:CGRectMake(100.f, 100.f, 100.f, 100.f) title:@"八点钟学院" image:[UIImage imageNamed:@"photo"]];
[btn addTarget:self action:@selector(panAction) forControlEvents:UIControlEventTouchDown];
[self.view addSubview:btn];
参考资料:
官方文档