看关于这方面的文章基本没有能涉及到UIGestureRecognizers
相关的文章,所以决定写这样一篇文章。也是个人第一篇文章,若有什么不对请及时指正。
本文主要经过一些实际测试来便于你们理解。算法
hitTest:withEvent:
和pointInside
来找到最优响应者,具体过程可参考下图hitTest
以及pointInside
时会先从view3开始便利,若是pointInside
返回YES就继续遍历view3的subviews(若是view3没有子视图,那么会返回view3),若是pointInside
返回NO就开始遍历view2。反序遍历,最后一个添加的subview开始。也算是一种算法优化。后面会具体介绍hitTest
的内部实现和具体使用场景。touchBegan:withEvent:
等方法,当gestureRecognizers找到识别的gestureRecognizer后,将会独自占有该touch,即会调用其余gestureRecognizer和hitTest view的touchCancelled:withEvent:
方法,而且它们再也不收到该touche事件,也就不会走响应链流程。下面会具体阐述UIContol和UIScrollView和其子类与手势之间的冲突和关系。测试hitTest和pointInside执行过程app
GSGrayView *grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)]; [self.view addSubview:grayView]; GSRedView *redView = [[GSRedView alloc] initWithFrame:CGRectMake(0, 0, grayView.bounds.size.width / 2, grayView.bounds.size.height / 3)]; [grayView addSubview:redView]; GSBlueView *blueView = [[GSBlueView alloc] initWithFrame:CGRectMake(grayView.bounds.size.width/2, grayView.bounds.size.height * 2/3, grayView.bounds.size.width/2, grayView.bounds.size.height/3)]; // blueView.userInteractionEnabled = NO; // blueView.hidden = YES; // blueView.alpha = 0.1;//0.0; [grayView addSubview:blueView]; GSYellowView *yellowView = [[GSYellowView alloc] initWithFrame:CGRectMake(CGRectGetMinX(grayView.frame), CGRectGetMaxY(grayView.frame) + 20, grayView.bounds.size.width, 100)]; [self.view addSubview:yellowView];
hitTest是一个递归函数ide
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { NSLog(@"-----%@",self.nextResponder.class); if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil; //判断点在不在这个视图里 if ([self pointInside:point withEvent:event]) { //在这个视图 遍历该视图的子视图 for (UIView *subview in [self.subviews reverseObjectEnumerator]) { //转换坐标到子视图 CGPoint convertedPoint = [subview convertPoint:point fromView:self]; //递归调用hitTest:withEvent继续判断 UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event]; if (hitTestView) { //在这里打印self.class能够看到递归返回的顺序。 return hitTestView; } } //这里就是该视图没有子视图了 点在该视图中,因此直接返回自己,上面的hitTestView就是这个。 NSLog(@"命中的view:%@",self.class); return self; } //不在这个视图直接返回nil return nil; }
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { NSLog(@"%@ -- pointInside",self.class); CGRect bounds = self.bounds; //若原热区小于200x200,则放大热区,不然保持原大小不变 //通常热区范围为40x40 ,此处200是为了便于检测 CGFloat widthDelta = MAX(200 - bounds.size.width, 0); CGFloat heightDelta = MAX(200 - bounds.size.height, 0); bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta); return CGRectContainsPoint(bounds, point); }
还用上面那个栗子:
点击redView:
redview -> grayView -> viewController -> ...
由于只实现到controller的touches事件方法所以只打印到Controller。函数
把上面栗子中的grayView替换成一个Button:oop
GSExpandButton *expandButton = [[GSExpandButton alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)]; expandButton.backgroundColor = [UIColor lightGrayColor]; [expandButton setTitle:@"点我啊" forState:UIControlStateNormal]; [expandButton addTarget:self action:@selector(expandButtonClick:) forControlEvents:UIControlEventTouchDown]; [self.view addSubview:expandButton]; self.redView = [[GSRedView alloc] initWithFrame:CGRectMake(0, 0, expandButton.bounds.size.width / 2, expandButton.bounds.size.height / 3)]; [expandButton addSubview:self.redView]; self.blueView = [[GSBlueView alloc] initWithFrame:CGRectMake(expandButton.bounds.size.width/2, expandButton.bounds.size.height * 2/3, expandButton.bounds.size.width/2, expandButton.bounds.size.height/3)]; // blueView.userInteractionEnabled = NO; // blueView.hidden = YES; // blueView.alpha = 0.1;//0.0; [expandButton addSubview:self.blueView]; self.yellowView = [[GSYellowView alloc] initWithFrame:CGRectMake(CGRectGetMinX(expandButton.frame), CGRectGetMaxY(expandButton.frame) + 20, expandButton.bounds.size.width, 100)]; [self.view addSubview:self.yellowView];
点击redView:
redview -> expandButton
布局
self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth, kScreenHeight * 3 / 4)]; [self.view addSubview:self.grayView]; self.tableView = [[GSTableView alloc] initWithFrame:CGRectMake(0, 20, kScreenWidth, self.grayView.bounds.size.height / 2)]; self.tableView.dataSource = self; self.tableView.backgroundColor = [UIColor darkGrayColor]; self.tableView.delegate = self; [self.grayView addSubview:self.tableView]; self.redView = [[GSRedView alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth/2, self.tableView.bounds.size.height/2)]; [self.tableView addSubview:self.redView];
点击redview
redview -> tableView
测试
在上面栗子中的view增长gestureRecognizer:优化
- (void)addGesture { GSGrayGestureRecognizer *grayGesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)]; [self.grayView addGestureRecognizer:grayGesture]; GSRedGestureRecognizer *redGesture = [[GSRedGestureRecognizer alloc] initWithTarget:self action:@selector(redViewClick:)]; [self.redView addGestureRecognizer:redGesture]; GSBlueGestureRecognizer *blueGesture = [[GSBlueGestureRecognizer alloc] initWithTarget:self action:@selector(blueViewClick:)]; [self.blueView addGestureRecognizer:blueGesture]; }
点击redView
打印结果以下图所示:
ui
// default is YES. causes touchesCancelled:withEvent: or pressesCancelled:withEvent: to be sent to the view for all touches or presses recognized as part of this gesture immediately before the action method is called. @property(nonatomic) BOOL cancelsTouchesInView; // default is NO. causes all touch or press events to be delivered to the target view only after this gesture has failed recognition. set to YES to prevent views from processing any touches or presses that may be recognized as part of this gesture @property(nonatomic) BOOL delaysTouchesBegan; // default is YES. causes touchesEnded or pressesEnded events to be delivered to the target view only after this gesture has failed recognition. this ensures that a touch or press that is part of the gesture can be cancelled if the gesture is recognized @property(nonatomic) BOOL delaysTouchesEnded;
cancelsTouchesInView
:默认为YES。表示当手势识别成功后,取消最佳响应者对象对于事件的响应,并再也不向最佳响应者发送事件。若设置为No,则表示在手势识别器识别成功后仍然向最佳响应者发送事件,最佳响应者仍响应事件。delaysTouchesBegan
:默认为NO,即在手势识别器识别手势期间,触摸对象状态发生变化时,都会发送给最佳响应者,若设置成yes,则在识别手势期间,触摸状态发生变化时不会发送给最佳响应者。delaysTouchesEnded
:默认为NO。默认状况下当手势识别器未能识别手势时,若此时触摸已经结束,则会当即通知Application发送状态为end的touch事件给最佳响应者以调用 touchesEnded:withEvent: 结束事件响应;若设置为YES,则会在手势识别失败时,延迟一小段时间(0.15s)再调用响应者的 touchesEnded:withEvent:。// button在上面 self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)]; GSGrayGestureRecognizer *graygesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)]; [self.grayView addGestureRecognizer:graygesture]; [self.view addSubview:self.grayView]; GSExpandButton *expandButton = [[GSExpandButton alloc] initWithFrame:CGRectMake(30, kTopHeight, 100, 100)]; expandButton.backgroundColor = [UIColor redColor]; [expandButton setTitle:@"点我啊" forState:UIControlStateNormal]; [expandButton addTarget:self action:@selector(expandButtonClick:) forControlEvents:UIControlEventTouchUpInside]; [self.grayView addSubview:expandButton];
点击button
this
从该栗子中能够看出即便下层view添加收拾依然会响应按钮的点击事件。
self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)]; GSGrayGestureRecognizer *graygesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)]; [self.grayView addGestureRecognizer:graygesture]; [self.view addSubview:self.grayView]; GSCancelledTouchView *cancelTouchView = [[GSCancelledTouchView alloc] initWithFrame:CGRectMake(30, kTopHeight, 100, 100)]; [self.grayView addSubview:cancelTouchView];
// 验证不取消button的touches事件猜想二 self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth/2, 400)]; GSGrayGestureRecognizer *graygesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)]; [self.grayView addGestureRecognizer:graygesture]; [self.view addSubview:self.grayView]; GSExpandButton *expandButton = [[GSExpandButton alloc] initWithFrame:CGRectMake(0, 0, kScreenWidth/3, 200)]; expandButton.backgroundColor = [UIColor redColor]; [expandButton setTitle:@"点我啊" forState:UIControlStateNormal]; [expandButton addTarget:self action:@selector(expandButtonClick:) forControlEvents:UIControlEventTouchUpInside]; [self.grayView addSubview:expandButton]; self.blueView = [[GSBlueView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; [expandButton addSubview:self.blueView];
UIControl及其子类可以执行点击事件而不是走底层的手势的缘由为:在识别到相应的gestureRecognizer后若是当前的最优响应者是UIControl及其子类而且当前的gestureRecognizer不是UIContol上的手势,则会响应UIControl的target:action:的方法。不然则会响应gestureRecognizer的target:action:的方法。
self.grayView = [[GSGrayView alloc] initWithFrame:CGRectMake(0, kTopHeight, kScreenWidth, kScreenHeight * 3 / 4)]; GSGrayGestureRecognizer *grayGesture = [[GSGrayGestureRecognizer alloc] initWithTarget:self action:@selector(grayViewClick:)]; // grayGesture.delaysTouchesBegan = YES; // grayGesture.cancelsTouchesInView = NO; // grayGesture.delaysTouchesEnded = YES; [self.grayView addGestureRecognizer:grayGesture]; [self.view addSubview:self.grayView]; self.tableView = [[GSTableView alloc] initWithFrame:CGRectMake(0, 20, kScreenWidth, self.grayView.bounds.size.height / 2)]; self.tableView.dataSource = self; self.tableView.backgroundColor = [UIColor darkGrayColor]; self.tableView.delegate = self; [self.grayView addSubview:self.tableView];
点击tableView
当父控件没有手势时
当父控件有手势时
当UIScrollView为最优响应者父控件有手势时,UIScrollView及其子类的点击代理方法和touchesBegan方法不响应。
解决方法:三种解决方式,我的认为第二种为最优解决方案
能够经过给父控件手势设置cancelsTouchesInView为NO,则会同时响应gestureRecognizer的事件和UIScrollView及其子类的代理方法和touches事件。
给父控件中的手势的代理方法里面作一下判断,当touch的view是咱们须要触发的view的时候,return NO ,这样就不会走手势方法,而去触发这个touch.view这个对象的方法了。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if ([NSStringFromClass([touch.view class]) isEqualToString:@"UITableViewCellContentView"]) { return NO; } return YES; }
文章如有不对地方,欢迎批评指正