iOS触摸事件传递响应之被忽视的手势识别器工作原理

1.写作缘起

在触摸事件传递机制这个的问题上连自己都觉着不就是老掉牙的Hit-Testingt么,递归遍历,找到最合适的view,然后把事件传递给它,如果它处理不了那就往它的下一个响应者传递,如果一直不能处理这个事件就将其丢弃.
不论是自己学习还是说给面试官都是认为就是这么回事,而且苹果的官方文档(点此处)也确实有这样的论述.

The hit-test view is given the first opportunity to handle a touch event. If the hit-test view cannot handle an event, the event travels up that view’s chain of responders as described in The Responder Chain Is Made Up of Responder Objects until the system finds an object that can handle it.

文档反复读了几遍,Hit-Testing Returns the View Where a Touch Occurred,这句话还有我们平时在开发中应用不断积累下来的理解我们很容易就总结出Hit-Testing就是找到了touch发生的那个view.然后就向上面的引用说的那样了,但是当晚上跟室友复现这个问题的时候,本来想着简直无懈可击啊,文档又不是第一次看,又不是没使用过这个原理解决问题.可是问题来了,自己问了自己一个问题:既然这个view可以处理这个事件,那么这个事件究竟是如何被处理的?换句话说手势是如何被识别出来的呢?仅仅是等待hit-test view判断不能处理之后再交给父view去处理么?假如是这样,那么如果罗列了100个view,每个view的手势不一样,有的是连续手势,比如缩放,那么以极限思维去思考,这个处理的时间是不是会像蜗牛一样呢?结论只有一个==我不知道也说不清楚这个具体的处理过程,想想自己对于这个问题之前的学习思考也太想当然了,群体都是盲从的,大部分的帖子也都是按照官方文档这个笼统的意思去解释的,当把这些问题抛出来给自己的时候,也就是这篇文章的缘起(对佛学有了解的同学们对缘起应该有更深刻的理解,顺便说一句,在下学禅多年,有同道中人可以一起学习).

2.解决疑惑--Google

资料不少,但是最为系统的论述还是苹果的文档(点此出),下面就直接说我理解出来的和我已经验证了的结果吧,当然还是建议大家把文档仔细读一读,写demo推敲,在下要是说错了感谢给予指正,先构建这样一个view的层级关系

view的层级

这里的view以及所加手势都继承写出自己的子类,这样我们就可以重写父类的方法了.代码如下

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createViewAndGes];
}

- (void)createViewAndGes
{
   //1.容器view
    WYContainerView *viewContainer = [[WYContainerView alloc]initWithFrame:self.view.bounds];
    [self.view addSubview:viewContainer];
    //--属性设置
    viewContainer.backgroundColor = [UIColor purpleColor];
    //--添加pinch手势
    WYPinchGesture * pinchges = [[WYPinchGesture alloc]initWithTarget:self action:@selector(pinchAction)];
    pinchges.delegate = pinchges;
    [viewContainer addGestureRecognizer:pinchges];
    
    //2.上部分的view
    WYViewUp * viewUp = [[WYViewUp alloc]initWithFrame:CGRectMake(0, 40, self.view.frame.size.width, 150)];
    [viewContainer addSubview:viewUp];
    //--属性设置
    viewUp.backgroundColor = [UIColor redColor];
    //--添加自定义的tap手势
    WYTapGesUp * tapUp = [[WYTapGesUp alloc]initWithTarget:self action:@selector(tapUpAction)];
    tapUp.delegate = tapUp;
    [viewUp addGestureRecognizer:tapUp];
    
    //3.下部分view
    WYViewDown *viewDown = [[WYViewDown alloc]initWithFrame:CGRectMake(0, 300, self.view.frame.size.width, 150)];
    [viewContainer addSubview:viewDown];
    //--属性设置
    viewDown.backgroundColor = [UIColor blueColor];
    //--添加自定义的手势
    WYTapGesDown *tapDown = [[WYTapGesDown alloc]initWithTarget:self action:@selector(tapDownAction)];
    tapDown.delegate = tapDown;
    [viewDown addGestureRecognizer:tapDown];
    
    
}

- (void)pinchAction
{
    NSLog(@"%s",__func__);
}

- (void)tapUpAction
{
    NSLog(@"%s",__func__);
}

- (void)tapDownAction
{
     NSLog(@"%s",__func__);
}
首先在每个view中重写下面的方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
    return [super hitTest:point withEvent:event];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
    [super touchesBegan:touches withEvent:event];
    
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    NSLog(@"%s",__func__);
    [super touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event
{
    NSLog(@"%s",__func__);
    [super touchesEnded:touches withEvent:event];
}
再次让每个手势子类都实现下面的方法
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    NSLog(@"%s",__func__);
    return YES;
}
结论如下:

当我们点击viewUp时,并不是等待viewUp完全判断自己不能处理这个事件之后再向下传递事件,而实际情况是这样的,手势识别器会先于绑定的view拿到这些touch,手势识别器中同样有touchesBegan:withEvent: 等touch方法,手势识别器是一个有限状态机,当hit-testing完毕,touch发生的view拿到之后(hit-test是一个递归),这条响应者链也就被app拿到了,此时touch开始向这条响应者链上的所有手势识别器分发,分发当然也得有个次序了,此时还是hit-test的手势识别器先拿到touch,然后状态机启动.哪个识别出:哦,这个无论是点击类型还是绑定的view跟我匹配,我触发action去了.
注意:此时是这链条上的所有手势识别器都会先于所绑定的view按一定次序开始触发状态机,不是依次等待上一个识别器有结果之后出发下一个,而且即使我们屏蔽了自定义view中touches方法,就是不调用super,那么手势识别器一样会触发action,也就是说view里面的touches方法并不影响手势的识别和事件的分发,屏蔽这个测试大家可以自己试一下
我们tap viewUp看一下控制台的打印,结果我们可以看出手势对象会先于所绑定的view拿到touch,并且绝不是viewUp的手势完全处理完毕后再让其父控件处理.

2016-11-13 13:45:57.690 ****混合手势****[22803:5468856] -[WYContainerView hitTest:withEvent:]
2016-11-13 13:45:57.690 ****混合手势****[22803:5468856] -[WYViewDown hitTest:withEvent:]
2016-11-13 13:45:57.691 ****混合手势****[22803:5468856] -[WYViewUp hitTest:withEvent:]
2016-11-13 13:45:57.691 ****混合手势****[22803:5468856] -[WYContainerView hitTest:withEvent:]
2016-11-13 13:45:57.691 ****混合手势****[22803:5468856] -[WYViewDown hitTest:withEvent:]
2016-11-13 13:45:57.692 ****混合手势****[22803:5468856] -[WYViewUp hitTest:withEvent:]
2016-11-13 13:45:57.692 ****混合手势****[22803:5468856] -[WYTapGesUp gestureRecognizer:shouldReceiveTouch:]
2016-11-13 13:45:57.693 ****混合手势****[22803:5468856] -[WYPinchGesture gestureRecognizer:shouldReceiveTouch:]
2016-11-13 13:45:57.694 ****混合手势****[22803:5468856] -[WYViewUp touchesBegan:withEvent:]
2016-11-13 13:45:57.695 ****混合手势****[22803:5468856] -[WYContainerView touchesBegan:withEvent:]
2016-11-13 13:45:57.820 ****混合手势****[22803:5468856] -[ViewController tapUpAction]

如果说那个代理方法不能让我们十分确认,那么我就就在viewUp的手势类中重写
touchesBegan:withEvent:方法,但是我们并不能调用super了,虽然此时不能响应action了,但是我们可以看到确实是手势识别器先拿到touches
打印结果如下:看第三行

2016-11-13 14:00:52.476555 ****混合手势****[5217:1428180] -[WYTapGesUp gestureRecognizer:shouldReceiveTouch:]
2016-11-13 14:00:52.476776 ****混合手势****[5217:1428180] -[WYPinchGesture gestureRecognizer:shouldReceiveTouch:]
2016-11-13 14:00:52.478008 ****混合手势****[5217:1428180] -[WYTapGesUp touchesBegan:withEvent:]
2016-11-13 14:00:52.479009 ****混合手势****[5217:1428180] -[WYViewUp touchesBegan:withEvent:]
2016-11-13 14:00:52.479221 ****混合手势****[5217:1428180] -[WYContainerView touchesBegan:withEvent:]
2016-11-13 14:00:52.522895 ****混合手势****[5217:1428180] -[WYViewUp touchesEnded:withEvent:]
2016-11-13 14:00:52.523205 ****混合手势****[5217:1428180] -[WYContainerView touchesEnded:withEvent:]

那么咱们看看官方文档是如何总结的

In the simple case, when a touch occurs, the touch object is passed from the UIApplication object to the UIWindow object. Then, the window first sends touches to any gesture recognizers attached the view where the touches occurred (or to that view’s superviews), before it passes the touch to the view object itself.

Gesture Recognizers Get the First Opportunity to Recognize a Touch
A window delays the delivery of touch objects to the view so that the gesture recognizer can analyze the touch first. During the delay, if the gesture recognizer recognizes a touch gesture, then the window never delivers the touch object to the view, and also cancels any touch objects it previously sent to the view that were part of that recognized sequence.

touch的传递次序

以上这些内容可以解释手势识别器的优先级是比所绑定的视图高的,而且也不是所有的touch都会传递到view,
到这里我们不妨再推敲一下,为什么要这样设计这个机制呢?不能等viewup判断自己能否处理之后再往下传递么?
答:如果是父view是缩放手势,如果按照依次传递会怎么样?可以看出在处理的时效性和准确性方面不如这么设计好.
那苹果的文档在hit-test说的就是最上面的view处理不了再交给后面的view啊?这不矛盾么?
答:这不矛盾,我们看文档不能断章取义,不能太机械,苹果在hit-test中说的是一种宏观上的表现形式.
hit-test的目标就是抓住touch对应的响应者链的头,这样我们就可以分发了,不然我们如何高效去分发呢?

最后我们探索一下这个响应者链的头是如何的关键

我们打破一下这个链条,看看这个头一旦拿错了后果是多么的有意思

#import "WYViewUp.h"

@implementation WYViewUp

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
    return [UIView new];
    return [super hitTest:point withEvent:event];
}

点击viewUp,看打印结果:

2016-11-13 14:31:19.914369 ****混合手势****[5232:1432814] -[WYContainerView hitTest:withEvent:]
2016-11-13 14:31:19.914694 ****混合手势****[5232:1432814] -[WYViewDown hitTest:withEvent:]
2016-11-13 14:31:19.914917 ****混合手势****[5232:1432814] -[WYViewUp hitTest:withEvent:]
2016-11-13 14:31:19.915918 ****混合手势****[5232:1432814] -[WYContainerView hitTest:withEvent:]
2016-11-13 14:31:19.916102 ****混合手势****[5232:1432814] -[WYViewDown hitTest:withEvent:]
2016-11-13 14:31:19.916263 ****混合手势****[5232:1432814] -[WYViewUp hitTest:withEvent:]

这里我们看到touch根本无法分发.

大胆猜想

苹果没有吧touches方法在手势识别器中暴露给我们估计是不方便我们使用,因为我们面对view更直接一些,不然我们需要用这些数据的时候还要剥离手势识别器,目前来看我们直接重写view就行了.

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

推荐阅读更多精彩内容