响应链与手势
事件响应链
通过事件传递找到最合适的view为第一响应者,其父view或viewcontroller则为第二响应者,以此递推,形成响应链。即pointInside返回true的view加上它们的控制器、UIWindow和UIApplication共同构成响应链。响应链的作用是找到一个和多个合适的view顺序的响应事件。响应链有如下规则:
- 超出父view范围的子view不能hitTest命中
- 父view禁止响应,影响到所有子view
- hitTest和touchesBegan是相反的顺序,即hitTest :v1->v2->v3, 若都命中则touchesBegan :v3->v2->v1
了解以下几个东西,能帮助理解响应链:
UIResponder
UIResponder是响应链的载体, 是UIKit的一个类,负责处理响应事件,是很多上层基本类的基类:
@interface UIView : UIResponder <NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, CALayerDelegate>
@interface UIViewController : UIResponder <NSCoding, UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>
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)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
...
hitTest
histTest简单而言,作用就是判断某个view是否被点击,通过下面一段代码,可以做到其判断过程:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// 1.判断自己能否接收触摸事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断触摸点在不在自己范围内
if (![self pointInside:point withEvent:event]) return nil;
// 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件
int count = self.subviews.count;
for (int i = count - 1; i >= 0; i--)
{
UIView *childView = self.subviews[i];
CGPoint childPoint = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childPoint withEvent:event];
if (fitView) { return fitView; }
}
// 没有找到比自己更合适的view
return self;
}
histTest是一个递归调用的过程:
- 判断自己能否接收触摸事件(是否不接受用户交互、隐藏、透明),否则返回nil
- 调用pointInside判断触摸点是否在自己范围,否则返回nil
- 广度优先搜索遍历子view重复上述过程
- 若子view没有histTest命中,则返回当前view通过重写histTest可以实现一些非常有用的功能,例如扩大按钮点击域、修改事件响应优先级等
手势
手势识别在 iOS 中非常重要,它极大地丰富了用户与iOS程序的交互方式。其使用步骤一般为: 创建手势->实现手势回调->为指定view添加手势,iOS中主要有多达7种手势。需要注意手势冲突等问题
手势与响应者链
- 手势与响应者链有一些差别,触摸事件首先会传递到手势上,如果手势识别成功,就会取消事件的继续传递。如果手势识别失败,事件才会被响应链处理
- 对于 UIButton,UISwitch,UISegmentedControl,UIStepper、UIPageControl 进行单击操作,如果父视图有轻敲手势需要识别,依然会按照响应链来处理,先响应这些控件的单击事件,这仅适用于与控件的默认操作重叠的手势识别。
UIResponder 和 UIControl
- UIResponder类:上承NSObject,下接UIView ,UIVIewController ,UIApplacation;响应点,压,滑,主要作用是响应某个动作,执行某个行为。
- UIControl类:上承UIView,下接UIButton、UISwitch等控件,在继承了UIResponder的属性基础上,还能够相应某个动作,为某个对象,添加动作(比如常见的addTarget函数)
手势冲突
1、如果一个手势A的识别部分是另一个手势B的子部分时,默认情况下A就会先识别,B就无法识别了。我们可以指定某个手势执行的前提是另一个手势失败才会识别执行,这样控制手势识别的响应顺序:
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;
例子如下:
UITapGestureRecognizer *tapGesture0 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick0:)];
[self.view addGestureRecognizer: tapGesture0];
UIView *testView = [[UIView alloc] initWithFrame:CGRectMake(20, 100, 200, 200)];
testView.backgroundColor = [UIColor redColor];
[self.view addSubview: testView];
UITapGestureRecognizer *tapGesture1 = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapClick1:)];
[self.view addGestureRecognizer: tapGesture1];
//设置tapGesture0优先级高于tapGesture1
//[tapGesture1 requireGestureRecognizerToFail: tapGesture0];
-(void)tapClick0:(UITapGestureRecognizer *)tap{
NSLog(@"点击tap0");
}
-(void)tapClick1:(UITapGestureRecognizer *)tap{
NSLog(@"点击tap1");
}
可以取消注释//[tapGesture1 requireGestureRecognizerToFail: tapGesture0];查看不同结果
2、如果同一视图需要一次响应多个手势操作,可以实现下面的UIGestureRecognizerDelegate的代理方法,当返回YES的时候,可以同时响应多个手势。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;
响应链另一种用法:ResponderChain
日常开发中,不同 UI 层级之间交互可有多种抉择:
- block
- 协议代理模式
- 通知
但是随着业务复杂提高上面的任何一种方式都会变得非常厚重,例如像直播间这种玩法多样的vc,最终可能会有几十种回调代码,不同回调的层数还各不一致,最终导致代码,此时我们需要考虑一种新的方式来达到我们目的。 假设有个直播控制器:liveVC,他的UI 层级是这样的:
4 个业务层view分别有各种回调需要listVC 处理,那么除了上述常规方式还有别的简洁的方案吗?答案是肯定的,我们可以基于响应链来达到目的,对 UIResponder 做如下分类:
@implementation UIResponder (Router)
- (void)routerEventWithName:(NSString *)eventName userInfo:(NSDictionary *)userInfo
{
[[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
@end
这样在最上层的view 处理交互事件时,直接调用routerEventWithName 函数即可通过 nextResponder 逐级查找到你想要实现交互事件回调的层级了。
既已览卷至此,何不品评一二: