嵌套UIScrollview的滑动冲突解决方案

作者: 没故事的卓同学 | 来源:发表于2017-05-02 23:58 被阅读11784次

    虽然苹果建议我们不要嵌套 UIScrollview ,但是这话到底是对开发者说的,架不住设计师就是要这么画设计图。
    我遇到的场景类似这样,顶部有一个优先展示的封面,当封面滚出屏幕后下面内容的 ScrollView 开始滚动。因为介绍、目录、评价三个tab是可以左右滚动的,所以具体内容是嵌套在一个 ScrollView 里的。

    但是其实面临的问题都是相似的,嵌套了 ScrollView 后,需要一个条件来控制某个具体时刻哪个 ScrollView 响应滑动事件。

    第一步就是通过设置 ScrollView 的 isScrollEnabled 属性来控制哪个响应。

        @IBOutlet weak var mainScrollView: UIScrollView!
        @IBOutlet weak var subScrollView: UIScrollView!
    
        let maxOffsetY: CGFloat = 150
    
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if scrollView == mainScrollView {
                if scrollView.contentOffset.y >= maxOffsetY {
                    mainScrollView.isScrollEnabled = false
                    subScrollView.isScrollEnabled = true
                }
            }else {
                // 当 subScrollView 滑动到顶时,停止响应,mainScrollView 开始响应。
                if scrollView.contentOffset.y <= 0 {
                    subScrollView.isScrollEnabled = false
                    mainScrollView.isScrollEnabled = true
                }
            }
        }
    

    在代理方法<code> scrollViewDidScroll </code>中进行判断,当mainScrollView 划出 150 后,停止响应滚动,这时手指再滑动,自然就是展示内容的 subScrollView 响应了。

    但是这个简单的方案在实际操作中有一个很大的弊端。用户并不总是完美的在滑动了 150 pt后,停下手。然后再开始进行下一次的滑动。如果用户一次性滚了 160 ,mainScrollView 就会在滚动到 150 后停止响应,此时因为是一次手势, subScrollView 也不会响应。用户就会有被中断的感觉,需要第二次滑动才能继续滚动。

    所以这种简单的粗暴的方案不能解决问题。毕竟用户是我们爸爸。

    大家都知道每个 ScrollView 都带了一个 panGestureRecognizer 。此时,我们开始幻想,要是可以自己控制这个 pan 手势就好了。
    于是我们写下了这行的代码开始进行尝试:

            mainScrollView.panGestureRecognizer.delegate = self
    

    大家都知道 iOS 的开发体验特别好,苹果的 API 都特别为大家着想。程序 run 起来后 xcode 就会给你一个 crash:

    'UIScrollView's built-in pan gesture recognizer must have its scroll view as its delegate.'

    我来翻译一下:


    解决的思路是我们自己添加一个联动的 pan 手势。注意在<code>UIGestureRecognizerDelegate</code>中的<code>shouldRecognizeSimultaneously</code>返回 true。

      let pan = UIPanGestureRecognizer(target: self, action: #selector(ViewController.panGestureRecognizerAction(recognizer:)))
      pan.delegate = self
      mainScrollView.addGestureRecognizer(pan)
    
      func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, 
                            shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
            return true
        }
    

    这样设置之后就可以在不影响滚动的情况下获取 pan 手势的参数。

    接着我们定义两个 bool 值判断当前滚动的 ScrollView

        var mainScrollEnabled = false
        var subScrollEnabled = false
    

    在 scrollViewDidScroll 中设置:

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
            if scrollView == mainScrollView {
                if scrollView.contentOffset.y >= maxOffsetY {
                    scrollView.setContentOffset(CGPoint(x: 0, y: maxOffsetY), animated: false)
                    mainScrollView.isScrollEnabled = false
                    subScrollView.isScrollEnabled = true
                    subScrollEnabled = true
                     mainScrollEnabled = false
                }
            }else {
                if scrollView.contentOffset.y <= 0 {
                    scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
                    subScrollView.isScrollEnabled = false
                    mainScrollView.isScrollEnabled = true
                    mainScrollEnabled = true
                    subScrollEnabled = false
                }
            }
        }
    

    接着我们在 pan 的手势的 recognizer 中响应处理这种滑动经过临界区域的情况:

    var currentPanY: CGFloat = 0
    
    func panGestureRecognizerAction(recognizer: UIPanGestureRecognizer) {
            if recognizer.state != .changed{
                currentPanY = 0
                // 每次滑动结束都清空状态
                mainScrollEnabled = false
                subScrollEnabled = false
            }else {
               let currentY = recognizer.translation(in: mainScrollView).y
               // 说明在这次滑动过程中经过了临界点
                if mainScrollEnabled || subScrollEnabled {
                    if currentPanY == 0 {
                        currentPanY = currentY  //记录下经过临界点是的 y
                    }
                    let offsetY = currentPanY - currentY //计算在临界点后的 offsetY
    
                    if mainScrollEnabled {
                       let supposeY = maxOffsetY + offsetY
                        if supposeY >= 0 {
                            mainScrollView.contentOffset = CGPoint(x: 0, y: supposeY)
                        }else {
                            mainScrollView.contentOffset = CGPoint.zero
                        }
                    }else {
                        subScrollView.contentOffset = CGPoint(x: 0, y: offsetY)
                    }
                }
            }
        }
    
    

    这种方案的缺点就是经过临界点的运动曲线会不自然。原生的滑动有个仿物理的弹性,而我们是直接设置 contentOffset 得到的,是线性的。

    补充

    iOS 嵌套UIScrollview的滑动冲突另一种解决方案的文章中补充了用 DynamicKit 模仿 ScrollView 的惯性。虽然复杂了一些但是确实效果好多了。

    欢迎关注我的微博:@没故事的卓同学

    相关文章

      网友评论

      • 此处已打码:给个DEMO撒
      • Bytesking:看下《我的诊所》APP里面有个去采购页面,这种嵌套处理的非常完美,思路很独特,我看过作者介绍,很牛叉,支持外面的TableView刷新,也支持内部的tableView刷新,只是配置一下就行
        哈秋呵呵:同求技术链接
        3e4e83573545:有技术分享的链接吗,
      • FR_Zhang:配图6的一笔
      • 阿木马克:UI tableview不就行了,tableheaderview是你放最上面内容的,然后tavleview有一种style不是可以让sectionheader到达顶部后固定的吗 ;然后在最近一步实现
        幸运星_c393:你想的简单了,下面每个选项都是一个tableview,你这是要tableview嵌套tableview?
      • 这是一只喵:这个需求刚写完不久,几乎每个外卖App上都有。写的时候各种尝试。断事件,截手势都尝试了,万脸懵逼。 后来用了和上面简单粗暴差不多的方案,当子Scroll滑动时,把对应的改变值传递给外面,外面进行相应变动。对于边界卡顿问题,原因是边界触发了阻尼效果,进行加速停止。最后尝试设置contentInset值,使滚动达到if条件时,未触发阻尼效果,速度不变,避免了中断问题。最终效果基本实现,无大问题。对比几大外卖巨头丝般顺滑的效果还有一定差距。 求博主一份demo,观摩下
      • wtqhy14615:不需要这么麻烦,shouldRecognizeSimultaneouslyWith返回true,直接根据外层Scrollview和内层ScrollView的contentoffset来调整彼此的contentoffset就行。
        wtqhy14615:@romancemystery 内层滑动到顶时,此时在内层的didscroll方法中,设置外层的contentoffset.y为一个固定值,制造一种外层不可再滑动的假象,反之亦然
        romancemystery:具体的思路能不能讲下呢?比如内层Scrollview滑动顶部了 作何处理
      • Lilin_Coder:其他还好,只不过mainScrollView和subTableView禁止滑动后无法响应ScrollView的代理方法,可能会对一些依赖ScrollViewDelegate的操作产生影响
      • 492647699e8d:有没有试过 今日头条的实现方式 ? 上面是一个webView,下面是一个TableView这种结构的! 他是一个ScrollView里面 嵌套了 上半部是webView下半部是TableView 切tableView 是有加载更多的功能的!
      • emmet7life:项目中用过一个很不错的处理这种情况的库,叫MXParallaxHeader,推荐给你,可以去研究学习一下。
        Lazyloading:这个不支持下边左右滑动啊
      • fengzilai:写过一个类似的,效果和之前的新浪微博首页差不多,用了子VC实现了下面的几个内容,外面主VC一个tableview,同在scrollViewDidScroll控制能不能滚动,为了解决楼主说的问题,用了两个通知控制到达顶部和离开顶部
        51aa90fc67ff::smile: :smile: 我也是这样子做
      • JopYin:卓哥有没有oc版的,解决同样问题demo
      • 黑点:没太理解UIScrollView嵌套是怎么回事 有空的话可以画个图 或者代码传GitHub展示一下吗
        我们的项目也是根据offset 实现的
      • 会飞的大西瓜v:一毛一样的需求:ghost::ghost::ghost::ghost:
      • 李小南:你这个头部可以滑动吗? 可以支持下拉刷新吗
      • 5e0a613374d3:正好我在写这个,确实是有冲突的,不过还是可以解决的,学到了
      • lexiaoyao20:这个的处理方式更简单,可以参考一下: https://github.com/lvXiaoPeng/LYEmbedScrollView
        吴文辉99:这个完美
        Everdinner:@MEalone 下拉刷新只是看你项目需要添加在哪里,如果需要每个子视图添加下拉刷新就不用给父视图添加下拉刷新,控制父视图的contentOffset也是可以的
        zooleebee:这个下拉刷新是在顶部,不是在中间位置
      • bigParis:呵呵, 路漫漫, 确实恶心啊. 用UIPanGestureRecognizer会导致不流畅, 产品不会允许的. 我是纯用contentOffset做的.
      • ddaa8dae50b0:顶部用单独的view,根据当前scrollview的contentOffset和Inset来算大小就好了。
        不用这么嵌套。
        吃蘑菇De大灰狼:@littlewish scrollView滚动的时候不停地修改frame吗?那往上滚动时候,顶部视图的subview布局呢?还会有这种效果吗?
        7c5345782d8d:@littlewish 如果顶部的图也需要手势呢?
        littlewish:@Ian_He 我们项目就是这样做的,根据偏移量去改顶部view的frame ,达到阈值就不变了。
      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/rg8mwl 欢迎点赞支持!
        欢迎订阅《iOS相关》https://toutiao.io/subject/183
      • 我的名字好长好长灬:crash
        我想问你一下哈,你这个Pan手势写在什么地方。
      • d9557f883fd8:这种效果???https://github.com/zhnnnnn/bantang
        d9557f883fd8:@要学很多东西 不是切换数据,是滑到最顶上
        买了否冷_:这个每次滑动切换item都重新加载数据?
      • Roader:这个得自己遇到了需求以后慢慢调试,光看一遍还是没有思路的

      本文标题:嵌套UIScrollview的滑动冲突解决方案

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