iOS-关于UIScrollView的嵌套联动

基本场景

(最终效果和链接在文末,支持Swift与OC)
UIScrollView嵌套多个UITableView的场景在APP里很常见,复杂点还有各种UITableView、UICollectionView各种嵌套的场景,目前通用的解决办法基本是在UIScrollView的代理方法
- (void)scrollViewDidScroll:(UIScrollView *)scrollView里比较偏移量和需要悬停的坐标位置再做相应处理,定义主要父视图scrollViewmainScrollView,嵌套的多个联动scrollViewcontentScrollView,先总结下大致思路。

  1. 手势响应,shouldRecognizeSimultaneouslyWithGestureRecognizer必须同时作用于mainScrollView和所有contentScrollView,而contentScrollView是需要横向滑动的,因此要允许同时垂直滑动,而不支持水平和垂直同时滑动。
  2. mainScrollView 、contentScrollView均需要实现scrollViewDidScroll并分别处理,两者的实际滑动是互斥的,同一时刻只有一方需要响应滑动,另一方做悬停处理,相互通知也很是麻烦。
  3. 下拉刷新,mainScrollView 、contentScrollView各自有着需要下拉刷新的场景,一般contentScrollView需要下拉刷新时也正好处于自身临界固定点的位置,这里也需要单独处理下。
  4. scrollsToTop 这其实是一个很容易被忽略的点,iOS系统有个小的隐藏功能,点击系统状态栏会查找到当前显示的UIScrollView并响应回到顶部,而在这种嵌套的场景里,主次需要响应的时机就依赖于需求了,或许需求就要求先回到contentScrollView后回到mainScrollView的顶部呢🤣。

要把这些都处理好,写代码的时候必须梳理清楚,即便如此,当项目不同模块都有着类似的需求的时候,又得好好捋一遍了,可能相似而不相同,一不小心就容易一团麻,令人抓狂。

之前在网上搜索这类需求的方案,大部分都是上述的大概思路,其他有些是对整个相关UI层的封装,一来学习使用成本略高,二则在已经成型的项目里使用的话,改动略大,耦合性比较高。于是打算自己重新整理一个低耦合的方案出来。

结果方案

依然是比较偏移量处理悬停,为了减少耦合,因此不走代理,采用KVO的方式监测偏移量,初始化需要设置mainScrollView和各contentScrollView,考虑到不少子页面可能存在懒加载的情况,因此contentScrollView可以不必在初始化时全给到,可延后等待时机添加。index参数用于标记contentScrollView在其横向父scrollView的位置,避免受到其他兄弟视图的滑动影响。

+ (instancetype)managerWithMainScrollView:(UIScrollView *)mainScrollView contentScrollViews:(NSArray<UIScrollView *> *_Nullable)contentScrollViews;
- (void)addContentScrollView:(UIScrollView *)contentScrollView withIndex:(NSInteger)index;

初始化完了,接下来就是本方案中唯一的必设属性了:

@property (nonatomic) CGFloat contentScrollDistance;

mainScrollView悬停相关的值,contentScrollView可以在mainScrollView移动的距离,一般是需要显示的内容区域在mainScrollView的相对坐标Y值,如图所示,箭头是终点,图中上面高为300,只要设置contentScrollDistance为300,就可以基本实现完整的嵌套联动了。当页面刷新高度变化的时候,只需要重新调整contentScrollDistance的值即可。

必设属性之后就是扩展需求的可选属性了。

///各contentScrollView的共同横向superScrollView
///内部是寻找第一个contentScrollView的父视图里的第一个UIScrollView
///与实际不符时可 以此修正
///主要用于scrollsToTop及散装属性
@property (nonatomic, weak) UIScrollView *fixHorizontalSuperScrollView;
///滑动条显示 默认切换显示
@property (nonatomic) XShowIndicatorType showIndicatorType;
///默认main可下拉
@property (nonatomic) XMixScrollPullType mixScrollPullType;
///点击状态栏回顶部时  是否直接回到mainScrollView顶部 默认Yes
@property (nonatomic) BOOL scrollsToMainTop;

///是否开启动态模拟 默认 NO  在main范围内content范围外 上拉没有过度滑动效果 YES则添加模拟效果
@property (nonatomic) BOOL enableDynamicSimulate;
///动态模拟过度滑动效果 阻力参数 默认 2
@property (nonatomic) CGFloat dynamicResistance;
  1. 如注释所示,该属性的出现主要是为了scrollsToTop的切换以及接下来要介绍的散装属性。
  2. mainScrollViewcontentScrollView各有各的滑动条,简单暴力的话就是全隐藏,但是毕竟contentScrollView可能上拉加载更多无限长,还是需要看情况显示的。
  3. 下拉刷新,可以自由设置mainScrollViewcontentScrollView是否支持下拉刷新。
  4. scrollsToMainTopNO时,点击状态栏会优先使当前contentScrollView回到顶部,其次回到mainScrollView顶部。
  5. 关于动态模拟,在滑动contentScrollView区域外的mainScrollView时,contentScrollView不会响应手势,自然也不会滑动,在惯性滑动过渡到contentScrollView的时候mainScrollView由于悬停设置会导致瞬停,没法好好平滑过渡,最终参考网上动态模拟的方案针对上滑触摸点在contentScrollView区域外mainScrollView区域内的单个场景增加了惯性模拟。因为需要额外的计算且不是必须的,所以默认关闭了。

以上关于contentScrollView的设置都是针对所有内容视图的,考虑到不同contentScrollView可能有着不同需求,比如有的子页面内容较少不需要显示滑动进度条,不需要回到子页面顶部,有的子页面内容可以无限上拉加载更多,需要进度条也需要回到子页面顶部之类的。因此增加了部分可选属性单独设置的方法。

///开启散装属性 默认NO
@property (nonatomic) BOOL enableCustomConfig;

- (void)setShowIndicatorType:(XShowIndicatorType)showIndicatorType forScrollView:(UIScrollView *)contentScrollView;
- (void)setMixScrollPullType:(XMixScrollPullType)mixScrollPullType forScrollView:(UIScrollView *)contentScrollView;
- (void)setScrollsToMainTop:(BOOL)scrollsToMainTop forScrollView:(UIScrollView *)contentScrollView;
- (void)setEnableDynamicSimulate:(BOOL)enableDynamicSimulate forScrollView:(UIScrollView *)contentScrollView;

没有单独设置属性的contentScrollView依然以主要设置为准。

大致实现

KVO那里判断代码比较长,大致说一下,KVO里在
mainScrollView 、contentScrollView的常规嵌套联动处理的基础上,加上了回到顶部、是否显示下拉状态的处理、以及惯性模拟的判断调用,此外对内容视图横向父scrollView的偏移量也添加了观察(如下),内容视图切换时需要校准scrollsToTop状态以及对散装进度条的显示状况进行修正。.p的写法只是为了少写几个associatedObject

  //横向父scrollView滑动处理
  NSInteger index = scrollView.contentOffset.x / scrollView.frame.size.width;
  if (scrollView.p.index != index) {
    scrollView.p.index = index;
    self.currentIndex = index;
    [self checkScrollsToTop];
    [self checkCustomConfig];
  }

联动的滑动过渡如下

- (void)changeMainScrollStatus:(BOOL)mainCanScroll
{
    if (self.mainScrollView.p.canScroll == mainCanScroll) {
        return;
    }
    self.mainScrollView.scrollsToTop = YES;
    self.mainScrollView.p.canScroll = mainCanScroll;
    for (UIScrollView *contentScrollView in self.contentScrollViews) {
        contentScrollView.p.canScroll = !mainCanScroll;
        if (mainCanScroll) {
            contentScrollView.contentOffset = CGPointZero;
        }
        if (!self.scrollsToMainTop) {
            contentScrollView.scrollsToTop = !mainCanScroll;
        }
    }
}

这里是到临界点过渡时的处理,canScroll = YES代表着主动滑动,反之则是悬停,被动跟滑,当mainScrollView可以滑动的时候重置下contentScrollView的偏移量。mainScrollView.scrollsToTop = YES则是因为在正好临界点时如果为NO则无法回到顶部,mainScrollView的实际scrollsToTop值会在KVO contentScrollView的偏移量大于0时重新赋值。

关于散装属性的处理比较简单,用字典存值,重写了属性的get方法。

最后是关于UIScrollView分类实现的这两个方法

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    if (self.p.markScroll) {
        //阻止横竖联动
        UIScrollView *scrollView = (UIScrollView *)otherGestureRecognizer.view;
        if ([scrollView isKindOfClass:[UIScrollView class]] && scrollView.p.markScroll) {
            return YES;
        }
    }
    //阻止其他意外联动
    return NO;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.p.scrollManager.enableDynamicSimulate) {
        [self.property.scrollManager.dynamicSimulate stop];
        if (self.p.isMain) {
            XMixScrollManager *scrollManager = self.p.scrollManager;
                scrollManager.isTouchMain = point.y < scrollManager.contentScrollDistance;
        }
    }
    return [super pointInside:point withEvent:event];
}

pointInside的处理,一是记录是否在需要模拟的坐标区间内滑动,二是停止之前的模拟。动态模拟本身就不多说了,想要了解的可以看文末的链接。

部分效果

简单UIScrollView嵌套
UITableView嵌套

链接

动态模拟部分参考->https://www.tuicool.com/articles/QVJnAbB
完整代码地址->XMixScrollManager
Swift版代码地址->XMixScrollManager_swift

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容

  • 在iOS中,滚动视图UIScrollView用于查看大于屏幕的内容。Scroll View有两个主要目的: 让用户...
    pro648阅读 38,103评论 4 37
  • 前言 在上一篇文章中,我们学习了三方刷新库MJRefresh(巧用MJRefresh),同时我们也说了MJRefr...
    langkee阅读 15,662评论 4 22
  • 一、简介 <<继承关系:UIScrollView --> UIView-->UIResponder-->NSObj...
    无邪8阅读 1,849评论 0 0
  • 1.UIScrollView是什么? 移动设备的屏幕⼤小是极其有限的,因此直接展⽰在用户眼前的内容也相当有限,当展...
    happycolt阅读 10,430评论 1 16
  • 大众对八字风水的态度 信与不信的 有兴趣的没兴趣的 想学习的不想学只想算算自己的 想借此探索自己命运走势的 想学会...
    028b47fc08dc阅读 300评论 0 0