美文网首页iOS相关技术实现iOS 开发
iOS-关于UIScrollView的嵌套联动

iOS-关于UIScrollView的嵌套联动

作者: xing3523 | 来源:发表于2019-12-26 21:59 被阅读0次

    基本场景

    (最终效果和链接在文末)
    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

    相关文章

      网友评论

        本文标题:iOS-关于UIScrollView的嵌套联动

        本文链接:https://www.haomeiwen.com/subject/ranlnctx.html