事件层次分析(一)

事件的传递和响应

  • UIResponder和响应链的组成

许多对象都继承自UIResponder,包括UIApplication对象,UIView​Controller对象以及所有的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)。

  1. touch事件:keywindow传递事件给事件发生的view(通过hitTest方法来递归找到)
  2. 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];

参考资料:
官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容