ScrollView、TableView、CollectionView 联动一种实现思路

ScrollView、TableView、CollectionView 联动一种实现思路

首先看看效果:

  1. 带悬停View头部的类型,可以上下拉刷新

scrollViewHoldHeader

2.不带悬停View头部的类型,可以上下拉刷新

(PS.图床上传GIF有问题,经常出现卡死问题,直接运行DEMO比较直观)

Demo地址

联动思路实现

核心思路

大部分处理方案

由于联动是在最底部的 ScrollView 上面添加一个 TableVIew 或者 CollectionView 之类的, 最大难点就是 手势处理 . 嵌套 ScrollView之后,需要一个条件来控制某个具体时刻那个 ScrollView 响应滑动事件。

通常,我们会在 ScrollView 代理方法里面

1
2
3
4
5
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

//blablabla 逻辑

}

例如,我们会实现这样一个逻辑,最底部有一个 mainScrollViewmainScrollView 上有一个 SubScrollView, 这样就类似一个双 ScrollView嵌套场景了

1
2
3
4
5
6
7
8
9
10
11
//写个伪代码
if (scrollView.contentOffset.y > maxOffsetY){
mainScrollView.isScrollEnabled = false;
subScrollView.isScrollEnabled = true;
}else{
// 当 subScrollView 滑动到顶部时,停止响应,mainScrollView 开始响应
if scrollView.contentOffset.y < 0{
subScrollView.isScrollEnabled = false;
mainScrollView.isScrollEnabled = ture;
}
}

代理方法里面 scrollViewDidScroll 中进行判断,当 mainScrollView 划出了 maxOffsetY ,停止响应滚动,继续滑动时候,就是 subScrollView 响应滚动了

但是,在实际操作上有个很大问题,使用者并不是总能滑动到 maxOffsetY 地方,停下手,进行下一次滑动,如果滑动大于 maxOffsetY 地方, mainScrollView 就会滚动到 maxOffsetY 之后,停止响应,是因为这是一次手势, subScrollView 也不会响应,使用者就有中断感觉,需要第二次滑动才能继续滚动

改变一下思路

由于有上面的不足,我们就改变一下思路.
大家都知道,每个 ScrollView 都带有一个 panGestureRecognizer 的手势,如果可以控制这个手势,就理论上能优化了,那么我们就有:

1
mainScrollView.panGestureRecognizer.delegate = self;

结果App 运行后报错:

‘UIScrollView’s built-in pan gesture recognizer must have its scroll view as its delegate’

解决的思路是我们自己添加一个联动的 pan 手势
UIGestureRecognizerDelegate里面有一个委托方法:

1
2
3
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}

只需要将其返回YES即可同时执行多种手势;如果手势之间或手势对应的视图有冲突或者有不同的需求,可以在这个方法里面添加一些相应的判断,例如一个uiview里面有3个手势分别是移动、缩放、旋转,而我只需要同时执行缩放和旋转的话:

1
2
3
4
5
6
7
8
9
10
11
12
// called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other
// return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
//
// note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPinchGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UIRotationGestureRecognizer class]]) {
return YES;
}
returnNO;
}

一句话总结就是此方法返回YES时,手势事件会一直往下传递,不论当前层次是否对该事件进行响应。

那么,我们可以这样做,在 mainScrollView实现这个代理,让手势传下去,给 subScrollView

回到Demo:
此Demo的 ScrollView 样子大概是这样:

如图, 红色框柱区域为 mainScrollView , 蓝色框柱地方为 HeaderView , 紫色框柱的地方是 subScrollView (CollectionView) ;

那么,如果需要悬停(就是红色 headerView 那部分,在 mainScrollView 上滑到最上面时候,卡在最上面, subScrollView 继续滑动)
实现思路类似上面所说的,在ScrollView代理中方法里面做文章

1
2
3
4
5
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

//blablabla 逻辑

}

UI布局

首先, subScrollView 是一个 Controller 里面的 CollectionView , mainScrollView 的Controller最底部那个View, 上面添加了一个 headerView(UIView可悬停) , 再添加一个可以左右切换的 segmentScrollView(请联系源码看) , subScrollView就是添加在这个可以左右切换的ScrollView 上,总体来说,mainScrollViewContentSize 就是 HeaderView的高度 + subScrollView里面CollectionView的高度 .

滑动传递

滑动到的时候,因为上面设置了 mainScrollView 的手势方法,可以响应多手势. 在拖着 subScrollView,即 CollectionView 的那个紫色区域时候, 底部的父视图的 mainScrollView也会收到响应,两个 ScrollView 都会进入各自 Controller(void)scrollViewDidScroll:(UIScrollView *)scrollView 方法里面,我们只要在各自方法里面写相应的逻辑判断,则会达到一个联动效果.

滑动逻辑

(具体详情可查看源码,这里说一个大概)

mainScrollView :

didScroll代理里面

1
2
3
4
5
6
7
8
9
10
11
12

//联动滑
CGFloat offsetY = scrollView.contentOffset.y;
self.isTopIsCanNotMoveTabViewPre = self.isTopIsCanNotMoveTabView;
if (offsetY >= self.scrollMinOffsetY + _headerViewTotalHeight ) {
scrollView.contentOffset = CGPointMake(0, _headerViewTotalHeight + self.scrollMinOffsetY);
self.isTopIsCanNotMoveTabView = YES;
}
else {
self.isTopIsCanNotMoveTabView = NO;
}

先说下这个逻辑, isTopIsCanNotMoveTabViewPreisTopIsCanNotMoveTabView 是一个全局变量,记录了当前状态与前一个状态是否能滑动状态,每次滑动时候,将根据是否达到了极限值而改变布尔类型

1
offsetY >= self.scrollMinOffsetY + _headerViewTotalHeight

因为一开始,mainScrollView的offset是在(0,0),向上拖动时候,offsetY增大, 当小于极限值时候 (如果是需要悬停状态,那么极限值就是 头部HeaderView高度 - 需要悬停高度) , 滚动的是 mainScrollView, subScrollViewcontentOffset 将会保持 (0,0) , 同理,如果大于极限值, mainScrollViewcontentOffset 也将会保持一个固定值,那么滑动的是 subScrollView , 两者之间将会用通知传递状态信息

注意

Demo中通知名使用的是唯一通知,好处就是,如果多个 Controller 也用到这种结构,在 NavigationController视图堆栈里面还没用清空销毁的 Controller 也会接收到另外 Controller通知,也会触发到 didScroll 方法里面的逻辑,所以,稳妥方法是不同 模块的Controller用唯一通知名

滑动到极限位置

test15227221269627

滑动到极限位置时候,HeaderView固定在极限位置, 即 MainScrollView 滑动到极限位置, 然后在 didScroll 代理方法里面,发通知给 SubScrollView ,让 SubScrollView可以滑动

1
2
3
4
5
6
7
8
9
10
11
12
if (self.isTopIsCanNotMoveTabView != self.isTopIsCanNotMoveTabViewPre) {
if (!self.isTopIsCanNotMoveTabViewPre && self.isTopIsCanNotMoveTabView) {
[[NSNotificationCenter defaultCenter] postNotificationName:_ScrollViewNestControllerViewGoTopNotificationName object:nil userInfo:@{_ScrollViewNestControllerViewCanScrollKey : @"1"}];
self.canScroll = NO;
}

if(self.isTopIsCanNotMoveTabViewPre && !self.isTopIsCanNotMoveTabView){
if (!self.canScroll) {
scrollView.contentOffset = CGPointMake(0, _headerViewTotalHeight + self.scrollMinOffsetY);
}
}
}

逻辑中,根据 canScroll 判断 submain是否能滑动

上下拉刷新回调

这套实现思路,能做到上下啦刷新,上拉刷新好说,就是在 mainScrollView 上面添加刷新控件,例如 MJReresh , 下拉刷新的话,需要再 subScrollView 上添加,因为,在 mainScrollView 固定不滚动时候,滚动的响应视图是 subScrollView , 所以 , 只能通过回调告诉 mainScrollView做相应操作,当然,如果你项目逻辑只需在 subScrollView里面完成就行的话,就不需要回调,一切视乎项目要求

最后

说下上面实现可能在项目中一些功能上,需求上不足

  1. 如果数据量太少,例如数据量不足一屏幕, CollectionView 或者 Tableview 创建时候,是基于一个全屏幕的,所以在滚动到极限位置,开始滚动 subScrollView 时候,下面会有很大一片空白位置, 暂时知道的 微博 也会存在这个问题,如果你们设计没有太大要求就没什么问题。 那么对于这种,解决方法也不是没有,CollectionView 通过
1
2
3
4
5
6

[CollectionView performBatchUpdates:^{

} completion:^(BOOL finished) {

}

这个方法,completion时候,能获得 collectionView 正真的 contentsize高度,那么可以判断是否少于屏幕高度尺寸,再直接设置 mainScrollViewsubScrollViewcontentSize 来不给他滚动太大距离即可, TableView就比较麻烦,需要提早缓存 高度,请求数据完成后,根据数据数量与每个缓存高度,计算是否少于屏幕尺寸也做响应操作。在左右切换 ScrollView 时候,也要动态即时改变,这个是比较麻烦的坑

另外如果有更好的解决方法,欢迎留言~谢谢

Reference:

  1. 嵌套UIScrollview的滑动冲突解决方案
  2. UIGestureRecognizer同时识别两个手势

ScrollView、TableView、CollectionView 联动一种实现思路

https://swlfigo.github.io/posts/5067/

Author

Sylar

Posted on

2019-04-28

Updated on

2021-11-14

Licensed under

Comments