看關於這方面的文章基本沒有能涉及到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; }
文章如有不對地方,歡迎批評指正