写在前面:这篇文章的重点在于讨论我想到的一些关于iOS触控响应这块容易忽略的问题,限于篇幅就不对基本的iOS触控响应知识做说明了。
一、iOS给我们提供的响应触控的方式
1、The touch-delivery methods
定义在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;
2、UIControl
即Target-Action模式,具体支持的事件在UIControlEvents
枚举中有定义。
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;
可以通过重写这四个tracking方法结合sendActionsForControlEvents:方法实现事件的自定义逻辑。
Note: 或许有人会有疑问说这四个tracking方法跟UIResponder中的四个touch-delivery methods方法很像,有什么区别呢?下文中的结论三会讲到。
3、UIGestureRecognizer
- UILongPressGestureRecognizer 长按
- UIPanGestureRecognizer 拖动
- UIPinchGestureRecognizer 缩放
- UIRotationGestureRecognizer 旋转
- UISwipeGestureRecognizer 轻扫
- UITapGestureRecognizer 点击
我们也可以继承UIGestureRecognizer,通过重写touch-delivery methods来自定义一个新手势。
二、为什么是多种方式
那么让我好奇的是为什么要提供三种方式呢?比如最常见的点击操作,通过上述三种方式都可以响应,那到底用哪种呢?为何不统一成一种方式呢?
我的理解,UIControl的出现是为了更好的封装性,把具有一些特定行为的UIView封装成控件,方便复用。而且通过demo实验可知,UIControl中对于UIControlEvents中定义的事件的支持,其实就是通过重写UIResponder中的touch-delivery methods实现的,不过加了一些特殊的逻辑(下文会提到)。
而UIGestureRecognizer是NS_CLASS_AVAILABLE_IOS(3_2)
加入的机制,我认为这是Apple对UIView的触控响应部分的解耦。同时,还有很重要的一点是UIGestureRecognizer的优先级是最高的,这个在网上很多关于触控响应的资料都有说明,下文中有我对于这块代码的实现猜想。
三、多种方式带来的混乱
为了便于对下文场景的描述我们约定一下简称:
UIView统一用view
UIScrollView统一用scrollView
UIButton统一用button
UITextField统一用textField
UIGestureRecognizer统一用gesture
声明:下文中描述的存在上下层级关系的view,都存在包含关系(即上层view都是addSubView到下层view上的)。显然,如果不存在包含关系,就不在一个响应链上,也就不需要讨论了。
- scrollViewA 上方有一个viewA,滑动viewA,scrollViewA会不会被滑动呢?它们的touch-delivery methods会触发吗?
- scrollViewA上方有一个scrollViewB,滑动scrollViewB,scrollViewA会不会被滑动呢?它们的touch-delivery methods会触发吗?
- buttonA上方有一个viewA,点击viewA,buttonA会响应吗?它们的touch-delivery methods会触发吗?
- buttonA上方有一个buttonB,点击buttonB,buttonA会响应吗?它们的touch-delivery methods会触发吗?
- viewA viewB viewC依次从下到上, viewA和viewB都addGestureRecognizer:tapGesture,那么点击viewC,viewA和viewB上的手势(都)会被触发吗?它们的touch-delivery methods会触发吗?
- 还是(5)这种场景,如果viewC是一个button,会怎样呢?
- buttonA上addGestureRecognizer:tapGesture,同时还addTarget:action:forControlEvents:UIControlEventTouchDown,在用户点击的时候它们都会被识别并触发吗?如果是UIControlEventTouchUpInside事件又会这样呢?
相信这些情况在一些复杂的业务场景中是可能碰到的,也会带来困惑。从下文中就可以找到答案了,我是通过最初的猜想,到自己写demo实验,得出的以下结论。
四、从猜想到实验得出的结论
结论一:UIResponder实现了UITouch的传递
@implementation UIResponder
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.nextResponder touchesBegan:touches withEvent:event];
}
...
etc.
@end
也就是我们重写touch-delivery methods时,如果不调用父类的实现,就会阻断UITouch的传递。
结论二:UIControl阻断了UITouch的传递
@implementation UIControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//没有调用下面这句
//[super touchesBegan:touches withEvent:event];
}
...
etc.
@end
基于结论一,再加以实验发现,UIControl在实现touch-delivery methods时就没有调用父类的实现,因此UIControl以及它的子类都会阻断UITouch的传递。
结论三:UIControlEvents只会在此UIControl是用户触摸的view时才会被触发
@implementation UIControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
if ([touch view] == self) {
//TODO: UIControlEvents中定义的事件的识别逻辑
[self beginTrackingWithTouch:touch withEvent:event];
}
}
//其它几个方法逻辑类似
...
etc.
@end
UIControl只有在hitTest:withEvent:方法最终返回的view是自己的时候,即用户真正触摸的view是自己的时候,才会触发响应。这样描述是为了说明,虽然UIControl也是继承了UIResponder并重写touch-delivery methods来实现触控响应逻辑,但是如果它不是在响应链的最外端,那么UIControlEvents事件并不会被触发。
Note:
可能有人会有疑问,代码中的//TODO: UIControlEvents中定义的事件的识别逻辑
为什么是在touchesBegan:withEvent:方法中实现?而不是封装到beginTrackingWithTouch:withEvent:中去?我也是经过实验得出的这个结论,我的理解是如果按后者来,那么我们在重写beginTrackingWithTouch:withEvent:方法时如果没有调用父类的实现,就使它失去了在父类里已经明确声明的对于UIControlEvents的支持,是不合理的,Apple如此设计做到了功能的一致性。(其实本该如此,只是我在猜想代码实现的的小想法而已)
结论四:UIButton会阻断superViews上的手势识别
我们知道手势响应的优先级是最高的,默认情况下,在UITouch的目标view及其superView上的gestureRecognizers被识别时,会调用目标view及其superView的touchesCancelled:withEvent:方法来取消响应链对于此UITouch的响应。除非把UIGestureRecognizer的cancelsTouchesInView属性设为NO。
但是有没有想过同样依赖touch-delivery methods来实现响应的UIButton跟比如UITapGestureRecognizer同时存在时,是怎样的响应逻辑呢?
直接上我通过实验后的猜想代码:
@implementation UIWindow
- (void)sendEvent:(UIEvent *)event {
UIView *resultView = nil;
NSSet <UITouch *> *allTouches = [event allTouches];
UITouch *touch = [allTouches anyObject];
for (UIView *view in self.subviews) {
CGPoint point = [touch locationInView:view];
resultView = [view hitTest:point withEvent:event];
if (resultView) {
//收集目标view及其superView的所有手势
NSArray *gestureRecognizers = [self collectGestureRecognizersWithLeafView:resultView];
//触发响应
switch (touch.phase) {
case UITouchPhaseBegan:
for (UIGestureRecognizer *gestureReconizer in gestureRecognizers) {
[gestureReconizer touchesBegan:allTouches withEvent:event];
}
[resultView touchesBegan:allTouches withEvent:event];
break;
case UITouchPhaseMoved:
for (UIGestureRecognizer *gestureReconizer in gestureRecognizers) {
[gestureReconizer touchesMoved:allTouches withEvent:event];
}
[resultView touchesMoved:allTouches withEvent:event];
break;
case UITouchPhaseCancelled:
for (UIGestureRecognizer *gestureReconizer in gestureRecognizers) {
[gestureReconizer touchesCancelled:allTouches withEvent:event];
}
[resultView touchesCancelled:allTouches withEvent:event];
break;
case UITouchPhaseEnded:
for (UIGestureRecognizer *gestureReconizer in gestureRecognizers) {
[gestureReconizer touchesEnded:allTouches withEvent:event];
}
[resultView touchesEnded:allTouches withEvent:event];
break;
default:
break;
}
break;
}
}
}
- (NSArray<UIGestureRecognizer*>*)collectGestureRecognizersWithLeafView:(UIView*)leafView {
NSMutableArray *gestureRecognizers = [NSMutableArray array];
if ([leafView isKindOfClass:[UIButton class]]) {
[gestureRecognizers addObjectsFromArray:leafView.gestureRecognizers];
}
else {
UIView *superView = leafView;
while (superView) {
[gestureRecognizers addObjectsFromArray:superView.gestureRecognizers];
superView = superView.superview;
}
}
return [gestureRecognizers copy];
}
@end
也就是如果UITouch的目标view是UIButton(也就是UIButton处在响应链的最外端),那么只有添加到这个UIButton上的手势会被识别,其余superViews上的手势都不会被识别。
结论五:默认情况下,比如UILongPressGestureRecognizer这种不能在UITouchPhaseBegan阶段就识别的手势,并不会影响UITouch的目标view以及其superView的touchesBegan:withEvent:方法的调用。除非把UIGestureRecognizer的delaysTouchesBegan属性设为YES。
我们在结论四里面对于UIWindow部分实现代码的猜想其实也能说明这一点,欠缺的是没有涉及对delaysTouchesBegan逻辑的猜想。
结论六:这一点算是基础知识,关于响应链上有多个手势存在的场景,其实就是对于UIGestureRecognizerDelegate的应用。
UIGestureRecognizerDelegate里有详细的注释说明,熟练掌握就可以灵活应对响应链上有多种手势存在的场景。
结论七:这一点也不是本文关注的重点,是我先前猜想的UIView的hitTest:withEvent:方法的实现逻辑,顺带贴出来便于理解这部分。
- (UIView*)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event {
//1、
if (self.userInteractionEnabled == NO
|| self.alpha == 0.0
|| self.hidden == YES) {
return nil;
}
//2、
if ([self pointInside:point withEvent:event] == NO) {
return nil;
}
//3、
UIView *result = nil;
for (UIView *view in self.subviews) {
result = [view hitTest:point withEvent:event];
}
//4、
return result ? result : self;
}
最后
写到这里必须声明,以上我说的都是错的。贴出来是希望跟大家交流学习,共同思考进步,阅读愉快!