1、响应链的传递
Responder一点也不神秘————iOS用户响应者链完全剖析(建议全看)
看完上面一篇应该能完全熟悉了响应链的传递,自己可以打印一下响应链看看,代码如下:
- (IBAction)click:(id)sender {
UIResponder *res = sender;
while (res) {
NSLog(@"*************************************\n%@",res);
res = [res nextResponder];
}
}
2、Hit-Test 机制
当用户触摸(Touch)屏幕进行交互时,系统首先要找到响应者(Responder)。系统检测到手指触摸(Touch)操作时,将Touch 以UIEvent的方式加入UIApplication事件队列中。UIApplication从事件队列中取出最新的触摸事件进行分发传递到UIWindow进行处理。UIWindow 会通过hitTest:withEvent:方法寻找触碰点所在的视图,这个过程称之为hit-test view。
hitTest 的顺序如下
UIApplication -> UIWindow -> Root View -> ··· -> subview
在顶级视图(Root View)上调用pointInside:withEvent:方法判断触摸点是否在当前视图内;
如果返回NO,那么hitTest:withEvent:返回nil;
如果返回YES,那么它会向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕。
如果有subview的hitTest:withEvent:返回非空对象则A返回此对象,处理结束(注意这个过程,子视图也是根据pointInside:withEvent:的返回值来确定是返回空还是当前子视图对象的。并且这个过程中如果子视图的hidden=YES、userInteractionEnabled=NO或者alpha小于0.1都会并忽略);
如果所有subview遍历结束仍然没有返回非空对象,则hitTest:withEvent:返回self;
系统就是这样通过hit test找到触碰到的视图(Initial View)进行响应。
如果还不清楚Hit-Test 机制,看更加清晰的Hit-Test 机制(建议还不清楚的看)
3、手势的原理及与touches系列的关系,具体的可以看iOS触摸事件传递响应之被忽视的手势识别器工作原理(建议不看也没关系,结论在下面了。)
简而言之,就是下面这幅图了。触摸事件会优先分发给附在view的手势,在这段延迟的期间,如果手势被识别,那么view的touches系列将被立刻取消,如果没有被识别,那么会继续我们所熟知的touches系列流程。
4、实际开发中常见的相关问题
在实际开发中,经常会遇到视图没有响应的情况,特别是新手会经常搞不清楚状况。
一下是视图没有响应的几个情况:
1.userInteractionEnabled=NO;
2.hidden=YES;
3.alpha=0~0.01;
4.没有实现touchesBegan:withEvent:方法,直接执行touchesMove:withEvent:等方法;
5.目标视图点击区域不在父视图的Frame上 (superView背景色为clear Color的时候经常会忽略这个问题)。
5、手势代理
ios手势识别代理,看这个基本上就够了。引用文章中的一段话,如下:
- 当时做项目时这个主控制器就是RootViewController,虽然用的是ScrollView但也没考虑到导航栏的手势返回的问题 ,现在做小区宝3.0的闪购订单,用之前的就有问题了。导航栏的返回手势用不了,根据响应者链和响应事件,手势被ScrollView识别了,就到不了导航的手势识别,所以导致无法手势返回。
我也曾经处理过这样的问题,不过我那时候是带有QQ的侧滑功能,主控制器用的View是ScrollView,导致不能侧滑。但是处理的方法都是一样的,自定义的ScrollView的代码重写gestureRecognizerShouldBegin方法如下,我是手势方向向右并且x轴起点小于60px的,让ScrollView的手势失效。这样就不会截获对应的事件了。但是其实看完上面,还有更简单的方法,就是让ScrollView的手势共存,但是这样可能会带来一些其它的问题。shouldRecognizeSimultaneouslyWithGestureRecognizer设置为true,不过应该要判断手势为UIScreenEdgePanGestureRecognizer时才return true,这样就可以了。
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
CGPoint velocity = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:self];
CGPoint location = [gestureRecognizer locationInView:self];
NSLog(@"velocity.x:%f----location.x:%d",velocity.x,(int)location.x%(int)[UIScreen mainScreen].bounds.size.width);
if (velocity.x > 0.0f&&(int)location.x%(int)[UIScreen mainScreen].bounds.size.width<60) {
return NO;
}
return YES;
}
案例分析
案例一
下面这种做法,除非你很熟悉,否则不要这么干。因为 [super touchesBegan:touches withEvent:event];会执行原来默认的操作,如果按钮本来就没有添加对应的事件。那么[[self nextResponder] touchesBegan:touches withEvent:event];和[super touchesBegan:touches withEvent:event];将会向下一响应者发送两次事件。
-(void)touchesBegan:(NSSet<uitouch *> *)touches withEvent:(UIEvent *)event{
if (self.enableNextResponder) {
[[self nextResponder] touchesBegan:touches withEvent:event];
[super touchesBegan:touches withEvent:event];
}
}
案例二
Window
-ViewA(能响应)
-ButtonA
-ViewB(不响应)
假设ViewB完全覆盖在ButtonA上,结果是:
ViewA能触发
Button没反应
ViewB没反应
简单来说,ViewB能阻隔ButtonA的响应,但是不能阻隔ViewA的响应。假设ViewB是个遮罩,那么并不能阻隔ViewA的事件触发。假设需要阻隔ViewA的相应,那么可以在ViewB添加空事件的相应,从而达到ViewB(遮罩)阻隔ViewA(如游戏层)的事件相应。
案例三
一个按钮添加了点击事件到底发生了什么事儿。
我们有时候需要使用到一些特殊的情况,比如:
1、A包含B,AB都响应事件。
对于普通View,根据响应链,让B作为第一个响应者处理,然后B根据nextResponder传递触摸事件。
针对手势做分析:
手势不会走view的touches系列方法,但有自己的一系列touches方法,不过没有暴露出来。但是shouldRecognizeSimultaneouslyWithGestureRecognizer也可以做到。
针对UIButton的分析:
UIButton addTarget分析,addTarget是UIControl的方法,其实addTarget的方法原理是,UIControl对touches的触摸事件的封装。[super touchesBegan:touches withEvent:event];包括了对
事件的封装处理,如果重新了[super touchesBegan:touches withEvent:event];,并且里面什么都不实现,那么当前UIButton添加的addTarget所绑定的所有事件都不会触发。因为覆盖了父类UIControl的封装方法。
如果我想一个按钮的事件触发,并且它的下一响应者也能触发相应的事件。那么该怎么处理呢?
我们在按钮上处理,重写touchesBegan系列的方法,那么根据上面所说,必须要调用super的方法,并且主动像下一响应者[self nextResponder]发送touchesBegan系列的方法。
2、A包含B、C,C在B的上面,但是想让B接收事件,C不接收事件
这种可以这么处理,自定义C的View,重写hitTest:withEvent方法,返回nil,这样自定义C的View及其子类都不会拦截事件。这样B就可以顺利处理事件。
还可以把C的userInteractionEnabled设置为NO
3、A是B、C的父视图,C在B的上面,这时候,CB都处理事件。这样到底行不行?根据响应链,这样应该是不靠谱的了。在C的touches方法中调用C的touches方法,然后重写B的touches方法,但是这样怪怪的。有什么高招也请多多指教。貌似也没有这样的必要。
最后还发现了一篇一步到位的iOS响应者链的全过程:iOS触摸事件的流动(想有更清晰的了解的看)
直接引用里面的一张图:
参考资料:
响应者链及相关机制总结