iOS 嵌套UIScrollView的滑动冲突解决方法

基本结构

  • 最外层是一个UITableView,称为mainScrollView
  • mainScrollView的最后一个section的header是一个menuBar,cell是一个UIScrollView,负责处理横向移动
  • UIScrollView里有四个UIScrollView,称为subScrollView

主要难点及解决方法

mainScrollView和subScrollView在滚动的时候会产生冲突

  • 当滚动subScrollView至临界点时,无法指定哪个UIPanGestureRecognizer被相应。如果将其中一个enable置为false,此时手势被中断,手指需要离开屏幕,重新滚动才能生效。
  • 解决方式:实现mainScrollView的shouldRecognizeSimultaneouslyWith方法,能同时识别两个gesture。
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return gestureRecognizer.isKind(of: UIPanGestureRecognizer.self) && otherGestureRecognizer.isKind(of: UIPanGestureRecognizer.self)
    }

滚动过程中对contentOffset的处理

  • 在滚动过程中需要将其中一个scrollView的contentOffset设为固定值,让另一个scrollView滚动。由于滚动时两个scrollView的contentOffset都会改变,所以判断条件经常会不满足。
  • 解决方式:在两个scrollView的scrollViewDidScroll方法里都进行处理。
func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard mainScrollView == scrollView else {
            return
        }
        if (subScrollView != nil && subScrollView!.contentOffset.y > 0) || scrollView.contentOffset.y > topCellHeight - fixHeight {
            mainScrollView.setContentOffset(CGPoint(x: 0, y: topCellHeight - fixHeight), animated: false)
        } else if scrollView.contentOffset.y < topCellHeight - fixHeight {
            for subController in self.childViewControllers {
                guard let vc = subController as? BaseViewController else {
                    return
                }
                vc.tableView.setContentOffset(.zero, animated: false)
            }
        }
    }
// 在subScrollView的scrollViewDidScroll方法里调用该方法
func subViewDidScroll(_ scrollView: UIScrollView) {
        subScrollView = scrollView
        if mainScrollView.contentOffset.y < topCellHeight - fixHeight {
            subScrollView.setContentOffset(.zero, animated: false)
        }
    }

惯性效果无法在内外层传递

  • 如果是OC代码,是有惯性传递效果的。
  • 当滑动subScrollView时,当到达临界值时,最初滚动的scrollView停止时,剩余的惯性不会传递出去。
  • 解决方式:先禁止subScrollView原有的线性减速逻辑,再用UIDynamicItem手动实现一个线性减速的效果。
    1、UIDynamicItem:用来描述一个力学物体的状态,其实就是实现了UIDynamicItem委托的对象,或者抽象为有面积有旋转的质点;
    2、UIDynamicBehavior:动力行为的描述,用来指定UIDynamicItem应该如何运动,即定义适用的物理规则。一般我们使用这个类的子类对象来对一组UIDynamicItem应该遵守的行为规则进行描述;
    3、UIDynamicAnimator;动画的播放者,动力行为(UIDynamicBehavior)的容器,添加到容器内的行为将发挥作用;
    4、ReferenceView:等同于力学参考系,如果你的初中物理不是语文老师教的话,我想你知道这是啥..只有当想要添加力学的UIView是ReferenceView的子view时,动力UI才发生作用。

DynamicItem的实例可以看作是一个质点, 在垂直方向上, 它的位置(center)可以用来计算两帧动画之间scrollView移动的距离, , 它的 transform 属性可以不用考虑.

// 遵循UIDynamicItem协议的质点(有位置速度,无尺寸)
class MyDynamicItem: NSObject, UIDynamicItem {
    var center: CGPoint = .zero
    var bounds: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
    var transform: CGAffineTransform
    override init() {
        transform = CGAffineTransform()
        super.init()
    }
}
// 禁止SubScrollView原有的线性减速逻辑
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
        DispatchQueue.main.async {
            scrollView.setContentOffset(scrollView.contentOffset, animated: false)
        }
        guard let delegate = delegate else { return }
        delegate.subViewWillEndDragging(scrollView, velocity: velocity.y * 500)
    }
var dynamicItem = MyDynamicItem()
var animator: UIDynamicAnimator?
var lastCenter: CGPoint = .zero

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    self.animator?.removeAllBehaviors()
}

// 在subScrollView的scrollViewWillEndDragging方法里调用该方法
func subViewWillEndDragging(_ subScrollView: UIScrollView, velocity: CGFloat) {
        if (velocity < 0 && subScrollView.contentOffset.y > 0) || (velocity > 0 && mainTableView.contentOffset.y < self.topCellHeight - self.fixHeight) {
            DispatchQueue.main.async {
                subScrollView.setContentOffset(subScrollView.contentOffset, animated: false)
            }
            dynamicItem.center = CGPoint(x: 0, y: mainTableView.contentOffset.y)
            lastCenter = dynamicItem.center
            let behavior = UIDynamicItemBehavior(items: [dynamicItem])
            behavior.addLinearVelocity(CGPoint(x: 0, y: velocity), for: dynamicItem)
            behavior.resistance = 2
            behavior.action = { [weak self] in
                guard let `self` = self else { return }
       
                if velocity < 0 { // 向下滑
                    let mainOffset = self.mainTableView.contentOffset.y
                    let subOffset = subScrollView.contentOffset.y
                    let scrollDistance = (self.lastCenter.y - self.dynamicItem.center.y)
                    if subOffset - scrollDistance <= 0 { // subScrollView滑动到顶部,需要把惯性传递给mainScrollView
                        subScrollView.contentOffset.y = 0
                        self.mainTableView.contentOffset.y = mainOffset - (scrollDistance - subOffset)
                    } else if self.bounceBehavior != nil { // 在回弹过程中,scrollDistance为负数,保证mainScrollView的offset不超过0
                        subScrollView.contentOffset.y = 0
                        self.mainTableView.contentOffset.y = min(mainOffset - (scrollDistance - subOffset), 0)
                    } else { // subScrollView未滑动到顶部,正常减速
                        subScrollView.contentOffset.y = subOffset - scrollDistance
                        self.mainTableView.contentOffset.y = self.topCellHeight - self.fixHeight
                    }
                } else if velocity > 0 { // 向上滑
                    let mainOffset = self.mainTableView.contentOffset.y
                    let subOffset = subScrollView.contentOffset.y
                    let scrollDistance = (self.dynamicItem.center.y - self.lastCenter.y)
                    if mainOffset + scrollDistance >= self.topCellHeight - self.fixHeight { // mainScrollView滑动到极限值,需要把惯性传递给subScrollView
                        self.mainTableView.contentOffset.y = self.topCellHeight - self.fixHeight
                        subScrollView.contentOffset.y = min(subOffset + mainOffset + scrollDistance - (self.topCellHeight - self.fixHeight), subScrollView.contentSize.height - subScrollView.frame.height)
                    } else { // mainScrollView滑动未到极限值,正常减速
                        self.mainTableView.contentOffset.y = mainOffset + scrollDistance
                        subScrollView.contentOffset.y = 0
                    }
                }
               
                self.lastCenter = self.dynamicItem.center
                
                self.bounceAnimate()
            }
            
            decelerateBehavior = behavior
            animator?.addBehavior(behavior)
        }
    }

弹簧效果

private func bounceAnimate() {
        let outsideFrame = mainTableView.contentOffset.y < 0
        
        if outsideFrame, let animator = self.animator, let _ = decelerateBehavior, bounceBehavior == nil {
            var target: CGPoint = .zero
            if mainTableView.contentOffset.y < 0 {
                dynamicItem.center = mainTableView.contentOffset
                target = .zero
                mainTableView.bounces = false
                let behavior = UIAttachmentBehavior(item: dynamicItem, attachedToAnchor: target)
                behavior.length = 0
                behavior.damping = 1
                behavior.frequency = 2
                
                self.bounceBehavior = behavior
                animator.addBehavior(behavior)
            }
        }
    }

你可能感兴趣的:(iOS 嵌套UIScrollView的滑动冲突解决方法)