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

虽然苹果建议我们不要嵌套 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
            }
        }
    }复制代码

在代理方法 scrollViewDidScroll 中进行判断,当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 手势。注意在UIGestureRecognizerDelegate中的shouldRecognizeSimultaneously返回 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
            }
        }else {
            if scrollView.contentOffset.y <= 0 {
                scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
                subScrollView.isScrollEnabled = false
                mainScrollView.isScrollEnabled = true
                mainScrollEnabled = true
            }
        }
    }复制代码

接着我们在 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 得到的,是线性的。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值