开发者i like code闻道丶iOS(大杂烩)

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

2016-11-13  本文已影响4614人  大爱无言

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就行了.

上一篇下一篇

猜你喜欢

热点阅读