事件、事件响应链、手势分析

一.事件

1.iOS三大事件包含触摸事件,设备移动事件,远程控制事件

2.iOS规定只有继承UIResponder的类才能处理事件

AppDelegate,,UIWindow,UIViewController,UIView都是继承它,具有处理事件的能力。
AppDelegate处理UIApplication的事件,UIViewController处理它的View的事件。

那么,我们看一下UIResponder中部分方法

//触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//设备移动事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event 
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event 
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event 
//远程控制事件
- (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event 

3.触摸事件

一个触摸点(一根手指)对应一个UITouch对象,每一个UITouch会产生一个UIEvent的对象,包含事件类型等信息,所以参数是NSSet<UITouch *> *类型。

比如写一个View随手指拖动的效果

//继承UIView的ZSView,重写touchmove方法
-(void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    UITouch * touch=[touches anyObject];
    CGPoint point=[touch locationInView:self];
    CGPoint prepoint=[touch previousLocationInView:self];
    NSLog(@"%@-%@",NSStringFromCGPoint(point),NSStringFromCGPoint(prepoint));
    CGFloat offsentx=point.x-prepoint.x;
    CGFloat offsenty=point.y-prepoint.y;
    [self setTransform:CGAffineTransformTranslate(self.transform, offsentx, offsenty)];
    
    //如果传nil,返回相对于当前window的CGPoint
//    CGPoint point2=[touch locationInView:nil];
//    CGPoint prepoint2=[touch previousLocationInView:nil];
//    NSLog(@"2:%@-%@",NSStringFromCGPoint(point2),NSStringFromCGPoint(prepoint2));
    //NSLog(@"%s",__func__);
}

二.事件传递

触摸事件的传递是从父控件传递到子控件,然后找到最合适的控件的过程;
UIApplication->UIWindow->UIView
如果父控件不接受事件,所以子控件都不能接受事件;

如何找到最合适的控件处理事件呢?
a.是否可以接受触摸事件
b.是否在自己身上
c.重复往前遍历子控件,重复ab
d.如果没有符合条件的子控件,就自己处理

代码实现就是hitTest的内部处理

//当前事件传递给当前View,当前View的hitTest方法会被调用,去寻找最合适的UIView,返回值就是最合适的UIView,然后调用该UIView的touches方法
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //1.是否可以接受触摸事件
    //2.是否在自己身上(pointInside方法)
    //3.从后往前遍历子控件,重复12
    //4.如果没有符合条件的子控件,就自己处理
    return [super hitTest:point withEvent:event];
}
//判断点击点在不在当前View身上,在hitTest内部调用(第2步)
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    //point表示当前触摸点
    return NO;
}

找几个例子一下就能明白:


1.png

1.重写各个View的touchesBegan

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@--touches",NSStringFromClass(self.class));
}

结果是:点击哪个View,哪个View的touchesBegan方法被调用(hitTest的自动实现)

2.重写各个View的hitTest,但是仅仅调用父类方法(相当于没重写)

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    return [super hitTest:point withEvent:event];
}

结果是:点击哪个View,哪个View的touchesBegan方法被调用(hitTest的自动实现)

3.重写红色View的hitTest,返回self

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    //如果事件传递到红色View,返回最合适UIView为自己
    return self;
}

结果是:点击灰色View和红色View,都是红色View的touchesBegan方法被调用
分析:点击灰色View,满足ab,再从后往前遍历子View,遍历到第一个红色View,返回最优响应者是红色View则停止遍历,点击红色也是同理。

4.重写灰色View的hitTest,返回第一个自View

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    return self.subviews[0];
}

结果是:点击灰色View及其子View,或者是蓝色View都是绿色View的touchesBegan方法被调用
分析:window遍历到vc.view的子控件时,满足ab,再从后遍历子View,遍历到第一个灰色View,返回最优响应者是其中第一个子控件也就是绿色View,点击其他View也是同理。

5.重写绿色View的hitTest,返回nil

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    return nil;
}

结果是:点击绿色和黑色都会让灰色View的touchesBegan方法被调用(穿透效果)
分析:点击绿色和黑色时,事件传递到绿色View,触发hitTest,返回空,也就是没和合适的View,所以回到上一层hitTest,也就是灰色View的hitTest,返回自己的View。

6.重写红色View的pointInside,返回YES

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return YES;
}

结果是:点击灰色View及其子View,都是红色View的touchesBegan方法被调用
分析:点击灰色View,满足ab,倒叙遍历子控件,遍历到红色View时,判断点击在红色区域内部,返回红色View;点击绿色或者黑色View,同样会遍历到红色View时被阻止向下遍历,所以也是红色ViewtouchesBegan方法被调用。

7.重写绿色View的pointInside,返回YES

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return YES;
}

结果是:点击灰色View,调用绿色View的touchesBegan,其余都正常
分析:自己分析吧,懒得写了。

响应链

当我们确定谁是那个最适合的UIView的时候,怎么构成一个响应链呢。
重写每个UIView的touchesBegan,调用父类touchesBegan方法

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%@--touches",NSStringFromClass(self.class));
    [super touchesBegan:touches withEvent:event];
}

结果是:
subView touches
superView touches
uiviewcontroller-view touches
window-touches
application-touches
也就是找到最合适处理事件的控件之后,调用touches,然后向上传递,这些响应者连接在一起构成了事件响应链。如果subview重写touches,就来到subview的touches,如果不重写,就来到superview的touches,如果superview不重写,就来到uiviewcontroller的touches,如果uiviewcontroller不重写,就来到window的touches,如果window不重写,就来到application(appdelegate)的touches,如果都不重写,这个事件就抛弃了。

脑子里回想两件事情,点击APP上一个按钮,如何找到被点击按钮的(命中测试),又如何再找到之后对事件做响应处理的(响应链传递),更容易帮你理解响应链的整体概念。

总结事件传递的完整过程:

a.先将事件由上向下(从父控件向子控件)传递,找到最合适处理事件的控件。
b.调用最合适控件的touches..方法
c.如果调用[super touches]方法,就会将事件顺着响应链条向上传递,传递给上一个响应者。
d.接着调用上一个响应者的touches方法

三.应用

最近发现触及到事件响应链的应用场景的两个问题,第一个是子View的大小超出父View的大小,但是还需要实现点击子View的效果(默认是不能点击超出父View区域的);第二个问题是子View覆盖在父View上,但是要实现穿透子View去响应父View点击事件,针对这两个问题,先说一下解决方案,再看一下响应链是如何做到的。

1.子View超出父View的情况:

ZSView * view =[[ZSView alloc]initWithFrame:CGRectMake(200, 400, 50, 50)];
view.backgroundColor=[UIColor lightGrayColor];
[self.view addSubview:view];
[view setUserInteractionEnabled:YES];

//UIButton * btn = [[UIButton alloc]initWithFrame:CGRectMake(100, -100, 200, 200)];
UIButton * btn = [[UIButton alloc]initWithFrame:view.bounds];
CGRect frame = btn.frame;
frame.size.width=100;
btn.frame=frame;
btn.center=CGPointMake(CGRectGetMidX(view.bounds), -CGRectGetMidY(btn.bounds) - CGRectGetMidY(view.bounds)+10);
btn.backgroundColor=[UIColor orangeColor];
[btn addTarget:self action:@selector(clickbtn) forControlEvents:UIControlEventTouchUpInside];
[view addSubview:btn];

重写父View的pointInside方法

@implementation ZSView
/*
如果点击区域在ZSView的范围内,point为正数,返回YES,否则对应x,y为负数,返回NO。所以我们重写该方法,把超出部分的point返回YES就可以,或者把超出子View的区域转换成自己View的坐标系,也就point为正数了。
*/
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    NSArray *subViews = self.subviews;
    if ([subViews count] > 0)
    {
        UIView *subview = [subViews objectAtIndex:0];
        if ([subview pointInside:[self convertPoint:point toView:subview] withEvent:event])
        {
            return YES;
        }
    }
    if (point.x > 0 && point.x < self.frame.size.width && point.y > 0 && point.y < self.frame.size.height)
    {
        return YES;
    }
    return NO;
}

2.穿透子View点击父View

UIButton * btn  = [[UIButton alloc]initWithFrame:CGRectMake(0, 0, 300, 300)];
btn.backgroundColor=[UIColor redColor];
[btn addTarget:self action:@selector(clickBtn) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];  
ZSTableView * tableview = [[ZSTableView alloc]initWithFrame:CGRectMake(50, 50, 200, 200)];
[tableview setUserInteractionEnabled:NO];
tableview.backgroundColor=[UIColor lightGrayColor];
tableview.delegate=self;
tableview.dataSource=self;
[btn addSubview:tableview];

重写子View(ZSTableView)的hitTest方法

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *hitView =[super hitTest:point withEvent:event];
    if(hitView == self){
        //自动将事件传递到上一层
        return nil;
    }
    return hitView;   
}

还有同学举到其他例子:

//www.greatytc.com/p/d8512dff2b3e

四.手势

1.手势分类

UITapGestureRecognizer 轻拍手势
UISwipeGestureRecognizer 轻扫手势
UILongPressGestureRecognizer 长按手势
UIPanGestureRecognizer 平移手势
UIPinchGestureRecognizer 捏合(缩放)手势
UIRotationGestureRecognizer 旋转手势
UIScreenEdgePanGestureRecognizer 屏幕边缘平移

基本使用就不介绍了,主要有一些常用手势属性。

2.UIPanGestureRecognizer,UIPinchGestureRecognizer

让UIView随平移手势拖动和捏合手势缩放

UIPanGestureRecognizer * pan=[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panView:)];
[self.testview addGestureRecognizer:pan];
    
UIPinchGestureRecognizer *pinch=[[UIPinchGestureRecognizer alloc]initWithTarget:self action:@selector(pinchView:)];
[self.testview addGestureRecognizer:pinch];

-(void)panView:(UIPanGestureRecognizer*)panGes
{
    //获得平移偏移量(相对于手势起始位置)
    CGPoint panPoint=[panGes translationInView:self.testview];
    //进行偏移累计
    self.testview.transform=CGAffineTransformTranslate(self.testview.transform, panPoint.x, panPoint.y);
    //重置偏移量(因为平移偏移是相对于起始位置,如果不重置该值,累计值就会叠加递增)
    [panGes setTranslation:CGPointZero inView:self.testview];
}

-(void)pinchView:(UIPinchGestureRecognizer*)pinchGes
{
    //进行缩放累计
    self.testview.transform=CGAffineTransformScale(self.testview.transform, pinchGes.scale, pinchGes.scale);
    //重置缩放范围
    [pinchGes setScale:1.0];
}

3.处理手势和UIView事件冲突

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