前几天遇到一个bug在一个collectionView上嵌套collectionView的页面, 下拉刷新以后会出现cell的点击事件全部不响应的问题. 这个问题查了很久才找到原因, 这里记录一下.
思路过程:
最开始遇到这个问题,以为是collectionView的frame错误导致的, 因为我们知道如果subView的位置超出了parentView的frame, 就会出现subView的点击事件不响应的问题。
但是排查以后发现collectionView
的frame
并没有问题, 而且虽然刷新以后cell不可以点击, 但是collectionView仍然可以滑动, 并且在滑动以后cell就可以恢复响应点击事件了。
随后发现注释掉加在collectionView
上面的自定义手势后, bug就不会出现了. 然后把问题归结为自定义手势和collectionView
的手势冲突导致的. 这里把自定义手势的cancelsTouchesInView
设置为NO
, 发现问题仍然存在,而且如果没有响应MJRefresh
的刷新时间,单独的滑动并不会产生这个问题。
这里又把问题转向了MJRefresh的代码,查看源码发现当MJRefreshHeader的state 是 Refreshing的时候,在设置scrollView的contentInset以后又设置了contentOffset。 代码如下:
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{
CGFloat top = self.scrollViewOriginalInset.top + self.mj_h;
// 增加滚动区域top
self.scrollView.mj_insetT = top;
// 设置滚动位置
CGPoint offset = self.scrollView.contentOffset;
offset.y = -top;
[self.scrollView setContentOffset:offset animated:NO];
} completion:^(BOOL finished) {
[self executeRefreshingCallback];
}];
});
然后注释掉 [self.scrollView setContentOffset:offset animated:NO]; 或者把这行代码替换为 self.scrollView.contentOffset = offset; 也不会出现这个bug了。
这里大概知道了,因为某种原因,scrollView在进入tracking状态以后,结束下拉刷新以后并没有退出tracking状态, 所以导致cell的点击事件都被暂停掉了,这时候再滑动scrollView, 才会让tracking状态改为NO。 cell响应恢复正常。
但是如果这里直接修改第三方的代码,会让后面第三方库的维护变的十分不方便。所以这里继续查是否还有其他方法可以解决这个问题。这时候发现scrollView有canCancelContentTouches
和delaysContentTouches
这两个属性,将这两个属性设置为NO以后cell恢复响应,这里以为问题到此结束,准备开开心心的提交代码。 结果在重新过了一遍其他相关功能的时候发现,在编辑状态拖拽cell结束的时候,cell会响应tap点击删除的动作。问题仍然存在。。。。
最后在下拉刷新结束以后调整contentSize, 发现可以改变scrollView的tracking状态。然后也不会造成其他的影响, 问题到这里算是暂时结束。
知识点
1.UIScrollView
的tracking
状态
tracking 表示scrollView正在被跟踪,从你的手指touch屏幕开始,scrollView开始一个timer,如果:
- 150ms内如果你的手指没有任何动作,消息就会传给subView。
- 150ms内手指有明显的滑动(一个swipe动作),scrollView就会滚动,消息不会传给subView,这里就是产生问题二的原因。
- 150ms内手指没有滑动,scrollView将消息传给subView,但是之后手指开始滑动,scrollView传送touchesCancelled消息给subView,然后开始滚动。
观察下tableView的情况,你先按住一个cell,cell开始高亮,手不要放开,开始滑动,tableView开始滚动,高亮取消。
delaysContentTouches
的作用:
这个标志默认是
YES
,使用上面的150ms的timer
,如果设置为NO
,touch事件立即传递给subview
,不会有150ms的等待。
cancelsTouches
的作用:
这个标准默认为
YES
,如果设置为NO
,这消息一旦传递给subview
,这scroll事件不会再发生。
cancelsTouchesInView
的作用:
文档上是这么描述的:
A Boolean value affecting whether touches are delivered to a view when a gesture is recognized.
通过设置这个布尔值,来设置手势被识别时触摸事件是否被传送到视图
通过设置这个布尔值,来设置手势被识别时触摸事件是否被传送到视图。
举个🌰
- (void)viewDidLoad {
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 50)];
button.backgroundColor = [UIColor colorWithRed:0.1 green:0.5 blue:0.4 alpha:1];
[supView addSubview:button];
[button addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapAction:)];
tap.cancelsTouchesInView = NO;
[button addGestureRecognizer:tap];
}
- (void)tapAction:(UITapGestureRecognizer *)sender {
NSLog(@"tap");
}
- (void)btnAction:(UIButton *)btn {
NSLog(@"button");
}
当cancelsTouchesInView
为NO
的时候,会分别触发tapAction:
和btnAction:
方法;而当cancelsTouchesInView
为YES
的时候,只会触发tapAction:
方法。
所以开始的时候,尝试把cancelsTouchesInView
设置为NO
,希望让collectionView同时相应自定义的手势和系统的点击事件。
2. setContentOffset: animated:
和 setContentOffset:
的区别
- 使用animated参数,可以获得正确的UIScrollViewDelegate的回调;而使用UIView动画则不能
在苹果的官方文档中,对setContentOffset:animated:
这一方法会引起的回调有大概如下的解释:
如果animated这一参数设置为NO,或者直接设置contentOffset这个property,delegate会收到一个
scrollViewDidScroll:
消息。如果animated这一参数设置为YES,则在整个动画过程中,delegate会收到一系列的scrollViewDidScroll:
消息,并且当动画完成时,还会收到一个scrollViewDidEndScrollingAnimation:
消息。
实验证明,使用setContentOffset:animated:
方法得到的回调行为和官方文档中描述的一致。而使用UIView动画,则只能收到一次scrollViewDidScroll:
回调,不能收到scrollViewDidEndScrollingAnimation:
回调。
- 使用animated参数,可以获取到动画过程中contentOffset的值
-
使用animated参数,即使
animated:
为NO
,scrollView
也会进入tracking
状态,而直接设置setContentOffset:
则不会。
所以在把MJRefreshHeader
的setContentOffset:animated:
方法注释掉以后, 因为scrollView
的tracking
状态没有变化,也不会出现bug
参考资料:
UIScrollView的delaysContentTouches与canCencelContentTouches属性