ios事件传递和响应机制


一,事件的分类

multitouch events

motion events

remote control events


Multitouch Events: 所谓的多点触摸事件,非常好理解,即用户触摸屏幕交互产生的事件类型。


Motion Events: 所谓的移动事件。是指用户在摇晃,移动和倾斜手机的时候产生的事件成为移动事件。这类事件依赖于iPhone手机里面的加速计,陀螺仪等传感器。


Remote Control Events:所谓的远程控制事件。这个事件从名称上面看,不太好理解。但其实,这个事件指的是用户在操作多媒体的时候产生的事件。比如,播放音乐、视频等。



更多的介绍motion events和remote control events?????


因为motion events和remote control events没有交互界面,ios系统为了支持这两类事件,题吹了Responder概念


二,什么是Responder

Responder的属性和方法,从下面的方法可以看出UIResponder可以处理Touchevent,motionevent,remote control event

- (UIResponder )nextResponder;
- (BOOL)canBecomeFirstResponder;    // default is NO
- (BOOL)becomeFirstResponder;
// Touch Event
- (void)touchesBegan:(NSSet<UITouch > )touches withEvent:(UIEvent )event;
- (void)touchesMoved:(NSSet<UITouch *> )touches withEvent:(UIEvent )event;
- (void)touchesEnded:(NSSet<UITouch *> )touches withEvent:(UIEvent )event;
- (void)touchesCancelled:(NSSet<UITouch *> )touches withEvent:(UIEvent )event;
// Motion Event
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent )event NS_AVAILABLE_IOS(3_0);
// Remote Control Event
(void)remoteControlReceivedWithEvent:(UIEvent)event NS_AVAILABLE_IOS(4_0);


注意有个很重要的方法,nextResponder,很明显可以看出来响应是一条链表结构,通过nestResponder找到下一个responder。这里是从第一个responder开始通过nextresponder传递事件,直到有responder响应了事件就停止传递;如果传到最后一个responder都没有被响应,那么该事件就被抛弃。

那么,谁是第一个resopnder呢?  responder是怎么响应的呢?responder响应后为什么不往下传递了呢?稍后会一一回答

1,UIResponder的衍生类

UIApplication UIViewController UIView都是继承UIResponder,都可以传递和响应事件


那么就可以这么理解,我们看到的一个界面,可能是由一个UIApplication和多个 UIViewController UIView组成,他们都是responder,他们一起组成了响应连。每次发生触摸事件,该事件就在这条响应链里传递


2,谁是第一个responder?

拿touchevent事件举例,一般情况下(因为有开放可以主动设置firstresponder),当前正在点击的视图对象就是first responder。


3,如何寻找first responder?


- (nullable UIView )hitTest:(CGPoint)point withEvent:(nullable UIEvent )event;
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;

hitTest和pointinside是系统遍历寻找firstresponder的方法。最终返回的view就是当前触摸的first responder


4,hitTest遍历寻找first responder 的规则是什么

1)首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
2)若返回NO,则hitTest:withEvent:返回nil;
3)若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;那么,后面的addsubview进来的子view就会优先被选中为first responder

4)若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;

注意:子view返回非空对象,若该子view还拥有自己的subviews,那么步骤3是个递归遍历。
5)若所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。


ios没有源码,下面是模拟源码写的

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *result = [super hitTest:point withEvent:event];
    CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
    if ([underButton pointInside:buttonPoint withEvent:event]) {
        return underButton;
    }
    return result;
}


从上面规则可以知道,视图超出父视图的区域是不会参与到遍历的,这是没有意义的计算。加上这个,一共有4种情况的view是不会参与到遍历的

1)隐藏(hidden=YES)的视图
2)禁止用户操作(userInteractionEnabled=NO)的视图
3)alpha<0.01的视图
4)视图超出父视图的区域

也就是说这四种情况的视图,以及他们的子视图是不会成为responder的。


下面通过一个比较直观的图形来讲述上面的规则


假设用户点击了视图D:
1) 检测到点击坐标在View A范围之内。
2) 继续检测点击范围是否在其子视图B,C范围内。发现点击范围在视图C范围内,则忽略掉B视图及其子视图分支。
3) 继续检测点击范围是否在其子视图D范围内,如果是,则用户当前视图即为视图D。如果不是,继续检测其子视图。

根据上述分析:iOS系统会从父视图向子视图依次查找,直到找到点击范围在当前视图边界范围以内。如果点击范围在某子视图范围内,并且没有了子视图,则该视图即为当前点击视图。如果点击范围在某子视图范围之内,并且不在其子视图范围之内,则点击视图即为当前点击视图。


事件的传递和响应

从上面可以看出,事件的传递方向是(hittest就是事件的传递):

UIApplication -> UIWindow ->ViewController-> UIView -> initial view

而Responder传递方向是(还记得nextResponder吗):
Initial View -> Parent View -> ViewController -> Window -> Application

如果最终传递到Application对象,依然没有对事件作出响应,事件就会被舍弃掉。


怎么样才算是对事件做出了响应呢?

在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[supertouches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法


如果一个view既实现了touches...方法,也调用了[super touched...],会不会出现一个事件被多个view处理呢?

答案是会的。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给下一个响应者处理
[super touchesBegan:touches withEvent:event]; 
}
而通常情况下,下一个响应者会是你的父view,和不是同级的其他view,这一点先记住,对后面的一个疑难问题解决很重要。


通常来说,子视图的nextResponder即为其父视图。如果子视图直接依附于ViewController,则该子视图的nextResponder即为其依附的ViewController

5,我们知道UIView是通过hitTest来传递事件的。那么UIViewController和UIApplication是通过什么来传递事件的呢?

6,主动设置了firstresponder后,他的事件传递和响应链又是怎样一条路径?

在应用的响应对象里,会有一个成为第一响应对象。
第一响应对象和其他响应对象之间有什么区别?对于普通的触摸事件没什么区别。就算
我把一个按钮设置成第一响应对象,当我点击其他按钮时,还是会响应其他按钮,而不
会优先响应第一响应对象。
第一响应对象的区别在于负责处理那些和屏幕位置无关的事件,例如摇动。
苹果官方文档的说法是:第一响应对象是窗口中,应用程序认为最适合处理事件的对象

7,UIApplication,UIViewController好像是没有hittest实现方法的,他们的事件传递和响应是怎么玩的?会是基于他们自己的view来传递事件的吗?



三,扩大热响应

按钮交互区域太小,想要扩大响应区,怎么办?

再次调出这段代码

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *result = [super hitTest:point withEvent:event];
    CGPoint buttonPoint = [underButton convertPoint:point fromView:self];
    if ([underButton pointInside:buttonPoint withEvent:event]) {
        return underButton;
    }
    return result;
}
刚才一直没有重点提到的方法pointInside: withEvent:,比较显然,如果你故意把point即使落在了button外面,也返回YES。
那么,重载UIButton的-(BOOL)pointInside: withEvent:方法,让Point即使落在Button的Frame外围也返回YES。
如下demo:
//in custom button .m
//overide this method
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}

CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
    CGRect hitTestingBounds = bounds;
    if (minimumHitTestWidth > bounds.size.width) {
        hitTestingBounds.size.width = minimumHitTestWidth;
        hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
    }
    if (minimumHitTestHeight > bounds.size.height) {
        hitTestingBounds.size.height = minimumHitTestHeight;
        hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
    }
    return hitTestingBounds;
}



四,点击穿透


点击区域1和3,自然是不用说了。

那么,点击区域2,是响应哪个事件呢?

通过上面的hittest遍历规则分析,我们该知道蓝色3区域是最后一个subview,后进先出,那么他应该是first reponder,所以点击2区域是响应蓝色按钮事件。


那么,既然我们的主题是事件穿透,那么肯定是希望红色2区域响应事件的。怎么做呢,肯定是要重写hittest方法的,基本思路就是在点击2区域返回的是红色button,那么等于改写了hittest规则,first responder就可以变成红色button。

那么,每个view都有hittest,我们是在哪个view重写hittest呢?

因为hittest在传递事件遍历的时候是从父view到subviews,而subviews是从蓝色到红色。

然而在点击2区域的时候,遍历到蓝色的时候就已经返回了,根本不会调用红色button的hittest。

所以,我们可以在蓝色view里重写,也可以在父view里重写hittest。下面是在蓝色view里重写的hittest

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint redBtnPoint = [self convertPoint:point toView:_redButton];
    if ([_redButton pointInside:redBtnPoint withEvent:event]) {
        return _redButton;
    }
    //如果希望严谨一点,可以将上面if语句及里面代码替换成如下代码
    //UIView *view = [_redButton hitTest: redBtnPoint withEvent: event];
    //if (view) return view;
    return [super hitTest:point withEvent:event];
}
如此,点击2区域,就会响应红色按钮就会响应事件。

这种透传有个前提:你要能获取到被挡住的view对象。

如果是私有的view对象,甚至通过runtime都获取不到该view对象。比如说appstore的下载按钮以及其他subviews,那就不能用这种方案。


五,全局响应


六,Gesture Recognizers与触摸事件分发

通过上面的知识,这里提一个问题,除了button这种有开放点击接口的view,对于普通uiview,你怎么区分点击事件、双击事件、长按事件、移动事件等呢?

我们知道事件的响应式在touches...方法里,这里有event,有touches,touches包含了你触摸的位置信息。

所以,你是可以根据touches的位置,以及位置的移动计算出来你到底是点击事件、还是其他事件。但是,我本来只是想做个其他功能而已,真的有必要为了这个事情浪费这么多时间吗?

答案是当然没必要。

Gesture Recognizers的提出就是为了解决该问题,它已经为你提供了常用的6个手势操作了,直接使用即可。

UITapGestureRecognizer:用来识别点击手势,包括单击,双击,甚至三击等。
UIPinchGestureRecognizer:用来识别手指捏合手势。
UIPanGestureRecognizer:用来识别拖动手势。
UISwipeGestureRecognizer:用来识别Swipe手势。
UIRotationGestureRecognizer:用来识别旋转手势。
UILongPressGestureRecognizer:用来识别长按手势

为了识别手势,需要将Gesture Recognizers关联到其检测触摸事件的view上,可以使用UIView的addGestureRecognizer:方法将手势识别器绑定到视图上。Gesture Recognizers在触摸事件处理流程中,处于观察者的角色,其不是view层级结构的一部分,所以也不参与responder chain。在将触摸事件发送给hit-test view之前,系统会先将触摸事件发送到hit-test view上绑定的或hit-test view父视图(superview)上绑定的Gesture Recognizers上。其流程大概如下图所示:


Gesture Recognizers与事件分发路径的关系


Gesture Recognizers可能会延迟将触摸事件发送到hit-test view上,默认情况下,当Gesture Recognizers识别到手势后,会向hit-test view发送cancel消息,来取消之前发给hit-test view的事件。这个地方,我持保留态度,我在log调试发现,hittest是先调用,而gesture recognizer是后被识别。


控制这个流程的是UIGestureRecognizer的三个属性
cancelsTouchesInView (默认为YES)
delaysTouchesBegan (默认为NO)
delaysTouchesEnded (默认为YES)
cancelsTouchesInView为YES,表示当Gesture Recognizers识别到手势后,会向hit-test view发送 touchesCancelled:withEvent:消息来取消hit-test view对此触摸序列的处理,这样只有Gesture Recognizers能响应此触摸序列,hit-test view不再响应。如果为NO,则不发送touchesCancelled:withEvent:消息给hit-test view,这样会使Gesture Recognizers和hit-test view同时响应触摸序列。


delaysTouchesBegan为NO,表示触摸序列开始时,而手势识别器还未识别出此手势时,touch事件会同时发向hit-test view,这样在手势识别器还未识别出此手势,hit-test view同时也可以收到同样的触摸事件。如果为YES,则在手势识别器在识别手势的过程中,不会有任何触摸事件发送给hit-test view,如果手势识别器最终识别到了手势,则也不会发送任何消息(包括touchesCancelled:withEvent:)给hit-test view;若干手势识别最终没有识别到手势,则所有的触摸事件在发给hit-test view处理。关于这个特性,可参考UIScrollView的delaysContentTouches属性。这样属性也谨慎使用,使用不当会导致UI无响应。


delaysTouchesEnded,在文档上的解释是,当手势识别器在识别手势时,对于UITouchPhaseEnded阶段的touch会延迟发送给hit-test view,在手势识别成功后,发送给hit-test view cancel消息,手势识别失败时,发送原来的end消息。其给出了了这样的例子识别双击操作的UITapGestureRecognizer对象,其numberOfTapsRequired设为2,在用户进行双击操作时,如果delaysTouchesEnded为NO,则hit-test view中的调用序列为
touchesBegan:withEvent:,
touchesEnded:withEvent:,
touchesBegan:withEvent:,
and touchesCancelled:withEvent:
如果delaysTouchesEnded为YES,则调用序列为:
touchesBegan:withEvent:,
touchesBegan:withEvent:,
touchesCancelled:withEvent:,
touchesCancelled:withEvent:
但我在实际测试时,并非如此,实际测试的结果是,如果delaysTouchesEnded为NO,则调用序列为:
touchesBegan:withEvent:,
touchesEnded:withEvent:,
TapGestureRecognizer 检测到双击


如果delaysTouchesEnded为YES,则调用序列为:
touchesBegan:withEvent:,
touchesEnded:withEvent:,
TapGestureRecognizer 检测到双击
touchesCancelled:withEvent:


问题:我如果需要在识别单击事件后,再通过hittest来确定谁是响应者


七,UIWebview的事件机制

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页