UIScrollView的滚动和触摸
一、UIScrollView原理
从你的手指touch屏幕开始,scrollView开始一个timer,如果:
- 150ms内如果你的手指没有任何动作,消息就会传给subView。
- 150ms内手指有明显的滑动(一个swipe动作),scrollView就会滚动,消息不会传给subView。
- 150ms内手指没有滑动,scrollView将消息传给subView,但是之后手指开始滑动,scrollView传送touchesCancelled消息给subView,然后开始滚动。
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event inContentView:(UIView *)view;
系统默认是允许UIScrollView,按照消息响应链向子视图传递消息的。(即返回YES)。如果你不想UIScrollView的子视图接受消息,返回NO。当返回NO,表示UIScrollView接收这个滚动事件,不必沿着消息响应链传递了。如果返回YES,touches事件沿着消息响应链传递;
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
返回YES 在这个view上取消进一步的touched消息(不在这个view上处理,事件传到下一个view)。如果这个参数view不是一个UIControl对象,默认返回YES。如果是一个UIControl 对象返回NO.
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都不会执行。
根据上面UIScrollView原理可知,150ms内手指有明显的滑动,scrollView就会滚动,消息不会传给subView。touchesShouldBegin系统默认是返回yes,也意味着只有消息传给subView时才会被触发。
测试二:如果先触摸拖拽滑动yellowview,不明显的滑动操作。控制台会打印
截屏2020-10-29 下午2.59.57.png
根据上面UIScrollView原理可知,150ms内手指没有滑动之后手指开始滑动,scrollView传送touchesCancelled消息给subView,然后开始滚动。
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view{
NSLog(@"不否将触摸事件传递给子控件:%s",__func__);
return NO;
}
- 如果你想直接拦截touch事件的传递,你直接返回NO就可以了。
小结:1、一个scrollView只有是明显的滑动时,才不会将触摸事件传递给子控件,其他情况下都会将触摸事件传给子控件。
2、直接重写touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView返回NO时也不会将触摸事件传给子控件。
3、当发生不明显的滑动,首先会将触摸事件传给子控件。但是当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,如果设置为NO,touch事件立即传递给subView,不会有150ms的等待。默认YES;如果设置为NO,会马上执行touchesShouldBegin:withEvent:inContentView:(不管你滑得有多快,都能将事件立即传递给subView)
canCencelContentTouches从字面上理解是“可以取消内容触摸“,默认值为YES。文档里的解释是这样的:翻译为中文大致如下:
这个BOOL类型的值控制content view里的触摸是否总能引发跟踪(tracking)
如果属性值为YES并且跟踪到手指正触摸到一个内容控件,这时如果用户拖动手指的距离足够产生滚动,那么内容控件将收到一个touchesCancelled:withEvent:消息,而scroll view将这次触摸作为滚动来处理。如果值为NO,一旦content view开始跟踪(tracking==YES),则无论手指是否移动,scrollView都不会滚动。
简单通俗点说,如果为YES,就会等待用户下一步动作,如果用户移动手指到一定距离,就会把这个操作作为滚动来处理并开始滚动,同时发送一个touchesCancelled:withEvent:消息给内容控件,由控件自行处理。如果为NO,就不会等待用户下一步动作,并始终不会触发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(@"点击了控制器");
}
根据开发经验我们可以清楚的知道,因为scrollView的存在,touchesBegan事件不会再被触发,很明显可以猜测到事件从window->scrollView,scrollView自己处理了touchesBegan:事件,并没有继续沿着响应链传递.
为了让它继续沿着响应链传递,我们就可以这样
@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(@"点击了控制器");
}
这时你会发现,touchesBegan似乎又没响应了。很显然可以知道肯定是添加手势影响的。没错,你只需要再添上这一行就如之前一样了。
gesture.cancelsTouchesInView = NO;
下面我们来分析一下原因吧:
UIScrollView 中有一个UIScrollViewDelayedTouchesBeganGestureRecognizer识别器,这个手势会截断hit-tested view事件并延迟0.15s才发送给hit-tested view。我们这里当点击屏幕时,首先会被gesture识别,而UIScrollViewDelayedTouchesBeganGestureRecognizer又会截断hit-tested view事件,当gesture识别完成后,hit-tested view事件也继续发送过去,就会被取消。当我们gesture.cancelsTouchesInView = NO;就不会再被取消,这样hit-tested view事件会继续沿着响应链进行传递和处理。
实际应用:点击键盘收回键盘
- (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