一、UIScrollView原理
從你的手指touch屏幕開始,scrollView開始一個timer,如果:
- 150ms內(nèi)如果你的手指沒有任何動作,消息就會傳給subView。
- 150ms內(nèi)手指有明顯的滑動(一個swipe動作),scrollView就會滾動,消息不會傳給subView。
- 150ms內(nèi)手指沒有滑動,scrollView將消息傳給subView,但是之后手指開始滑動,scrollView傳送touchesCancelled消息給subView,然后開始滾動。
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event inContentView:(UIView *)view;
系統(tǒng)默認是允許UIScrollView,按照消息響應(yīng)鏈向子視圖傳遞消息的。(即返回YES)。如果你不想UIScrollView的子視圖接受消息,返回NO。當(dāng)返回NO,表示UIScrollView接收這個滾動事件,不必沿著消息響應(yīng)鏈傳遞了。如果返回YES,touches事件沿著消息響應(yīng)鏈傳遞; 例如:scrollView上添加一個按鈕,返回NO的話,scrollView將不會將觸摸事件傳遞給按鈕,按鈕的event事件就不響應(yīng)。
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
返回YES 在這個view上取消進一步的touched消息(不在這個view上處理,事件傳到下一個view)。如果這個參數(shù)view不是一個UIControl對象,默認返回YES。如果是一個UIControl 對象返回NO。 返回NO它會停止拖動,將觸摸事件傳遞給子實圖例如scrollView上添加一個按鈕,按鈕是UIControl,返回NO,所以拖拽按鈕將不會滾動,為了能繼續(xù)滾動需要重寫這個方法返回YES;另外當(dāng)設(shè)置canCancelContentTouches = NO時,這個方法將不會被調(diào)用。
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
if ([view isKindOfClass:UIButton.class]) {
return YES;
}
return [super touchesShouldCancelInContentView:view];
}
例子
MyScrollView *scrollView = [[MyScrollView alloc]init];
[self.view addSubview:scrollView];
scrollView.frame = CGRectMake(0, 0, self.view.bounds.size.width, 400);
scrollView.backgroundColor = [UIColor redColor];
scrollView.contentSize = CGSizeMake(0, self.view.frame.size.height);
UIView *yellowview = [[GreenView alloc]init];
yellowview.backgroundColor = [UIColor yellowColor];
yellowview.frame = CGRectMake(100, 200, 200, 400);
[scrollView addSubview:yellowview];
@interface MyScrollView : UIScrollView
@end
@implementation MyScrollView
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view{
BOOL inContinue = [super touchesShouldBegin:touches withEvent:event inContentView:view];
NSLog(@"是否將觸摸事件傳遞給子控件:%s",__func__);
return inContinue;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view{
BOOL cancel = [super touchesShouldCancelInContentView:view];
NSLog(@"是否取消進一步的touched消息:%s",__func__);
return cancel;
}
測試一:如果手指快速滑動yellowview ,很明顯的滑動操作,控制臺不會打印任何東西。touchesShouldBegin和touchesShouldCancelInContentView都不會執(zhí)行。
根據(jù)上面UIScrollView原理可知,150ms內(nèi)手指有明顯的滑動,scrollView就會滾動,消息不會傳給subView。touchesShouldBegin系統(tǒng)默認是返回yes,也意味著只有消息傳給subView時才會被觸發(fā)。
測試二:如果先觸摸拖拽滑動yellowview,不明顯的滑動操作。控制臺會打印

根據(jù)上面UIScrollView原理可知,150ms內(nèi)手指沒有滑動之后手指開始滑動,scrollView傳送touchesCancelled消息給subView,然后開始滾動。
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view{
NSLog(@"不否將觸摸事件傳遞給子控件:%s",__func__);
return NO;
}
- 如果你想直接攔截touch事件的傳遞,你直接返回NO就可以了。
小結(jié):1、一個scrollView只有是明顯的滑動時,才不會將觸摸事件傳遞給子控件,其他情況下都會將觸摸事件傳給子控件。
2、直接重寫touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView返回NO時也不會將觸摸事件傳給子控件。
3、當(dāng)發(fā)生不明顯的滑動,首先會將觸摸事件傳給子控件。但是當(dāng)scrollView開始滑動時,scrollView傳送touchesCancelled消息給subView又會取消觸摸事件。
二、delaysContentTouches和canCancelContentTouches
@property(nonatomic) BOOL delaysContentTouches;
default is YES. if NO, we immediately call -touchesShouldBegin:withEvent:inContentView:. this has no effect on presses
@property(nonatomic) BOOL canCancelContentTouches;
default is YES. if NO, then once we start tracking, we don't try to drag if the touch moves. this has no effect on presses
delaysContentTouches的作用:
這個標志默認是YES,使用上面的150ms的timer,如果設(shè)置為NO,touch事件立即傳遞給subView,不會有150ms的等待。默認YES;如果設(shè)置為NO,會馬上執(zhí)行touchesShouldBegin:withEvent:inContentView:(不管你滑得有多快,都能將事件立即傳遞給subView)
canCencelContentTouches從字面上理解是“可以取消內(nèi)容觸摸“,默認值為YES。文檔里的解釋是這樣的:翻譯為中文大致如下:
這個BOOL類型的值控制content view里的觸摸是否總能引發(fā)跟蹤(tracking)
如果屬性值為YES并且跟蹤到手指正觸摸到一個內(nèi)容控件,這時如果用戶拖動手指的距離足夠產(chǎn)生滾動,那么內(nèi)容控件將收到一個touchesCancelled:withEvent:消息,而scroll view將這次觸摸作為滾動來處理。如果值為NO,一旦content view開始跟蹤(tracking==YES),則無論手指是否移動,scrollView都不會滾動。
簡單通俗點說,如果為YES,就會等待用戶下一步動作,如果用戶移動手指到一定距離,就會把這個操作作為滾動來處理并開始滾動,同時發(fā)送一個touchesCancelled:withEvent:消息給內(nèi)容控件,由控件自行處理。如果為NO,就不會等待用戶下一步動作,并始終不會觸發(fā)scrollView的滾動了。
三、UIScrollView和hitTested-view
MyScrollView*scrollView =[[MyScrollView alloc]init];
scrollView.MyDelegate = self;
[self.view addSubview:scrollView];
scrollView.backgroundColor =[UIColor yellowColor];
scrollView.frame = self.view.bounds;
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"點擊了控制器");
}
根據(jù)開發(fā)經(jīng)驗我們可以清楚的知道,因為scrollView的存在,touchesBegan事件不會再被觸發(fā),很明顯可以猜測到事件從window->scrollView,scrollView自己處理了touchesBegan:事件,并沒有繼續(xù)沿著響應(yīng)鏈傳遞.
為了讓它繼續(xù)沿著響應(yīng)鏈傳遞,我們就可以這樣
@implementation MyScrollView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[self.nextResponder touchesBegan:touches withEvent:event];
}
第二個例子:和上面基本上差不多的代碼,只是添加了一個手勢識別器
- (void)viewDidLoad {
[super viewDidLoad];
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap:)];
[self.view addGestureRecognizer:gesture];
MyScrollView*scrollView =[[MyScrollView alloc]init];
[self.view addSubview:scrollView];
scrollView.backgroundColor = [UIColor yellowColor];
scrollView.frame = CGRectMake(0,0 , 100, 100);
}
- (void)tap:(UIGestureRecognizer*)geture{
NSLog(@"測試");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"點擊了控制器");
}
這時你會發(fā)現(xiàn),touchesBegan似乎又沒響應(yīng)了。很顯然可以知道肯定是添加手勢影響的。沒錯,你只需要再添上這一行就如之前一樣了。
gesture.cancelsTouchesInView = NO;
下面我們來分析一下原因吧:
UIScrollView 中有一個UIScrollViewDelayedTouchesBeganGestureRecognizer識別器,這個手勢會截斷hit-tested view事件并延遲0.15s才發(fā)送給hit-tested view。我們這里當(dāng)點擊屏幕時,首先會被gesture識別,而UIScrollViewDelayedTouchesBeganGestureRecognizer又會截斷hit-tested view事件,當(dāng)gesture識別完成后,hit-tested view事件也繼續(xù)發(fā)送過去,就會被取消。當(dāng)我們gesture.cancelsTouchesInView = NO;就不會再被取消,這樣hit-tested view事件會繼續(xù)沿著響應(yīng)鏈進行傳遞和處理。
實際應(yīng)用:點擊鍵盤收回鍵盤
- (void)viewDidLoad {
[super viewDidLoad];
UITextField *textFiled =[[UITextField alloc]init];
[self.view addSubview:textFiled];
// textFiled.frame = CGRectMake(0, 0, 100, 40);
textFiled.backgroundColor = [UIColor redColor];
[textFiled becomeFirstResponder];
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap)];
[self.view addGestureRecognizer:gesture];
gesture.cancelsTouchesInView = NO;
UITableView *tableView = [[UITableView alloc]init];
tableView.frame = self.view.bounds;
[self.view addSubview:tableView];
tableView.dataSource = self;
tableView.delegate = self;
tableView.estimatedRowHeight = 0 ;
tableView.estimatedSectionHeaderHeight = 0;
tableView.estimatedSectionFooterHeight = 0;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
return cell;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
NSLog(@"%@",touch.view);
return YES;
}
#pragma mark - 點擊鍵盤收回鍵盤
- (void)tap{
[self.view endEditing:YES];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(@"點擊cell");
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 10;
}
@end