iOS嵌套scrollView联动加顶部悬浮效果

先上效果图:


效果图

原理:

1、主视图是一个scrollView
2、scrollView上添加一个header(随scrollView滚动)、sectionHeader(悬浮在顶部)、内容视图contentView(与主scrollView联动)
3、contentView也是一个scrollView,用于存放子视图,子视图为多个scrollView
类似下图:


布局示意图

实现方案:

主视图:

1、使用代理方法获取header、sectionHeader、contentViews,以及各自的高度

/// 资源代理
protocol LGNestViewDataSource: NSObjectProtocol {
    /// 头部视图
    func LGNestViewHeaderView() -> UIView
    /// 头部悬浮视图
    func LGNestViewSectionHeaderView() -> UIView
    /// 头部视图高度
    func LGNestViewHeaderViewHeight() -> CGFloat
    /// 头部悬浮视图高度
    func LGNestViewSectionHeaderViewHeight() -> CGFloat
    /// 内容视图
    func LGNestViewContentViews() -> [LGNestContentView]
}

代理方法回调scrollView滚动距离

/// 视图滚动代理方法
protocol LGNestViewDelegate: NSObjectProtocol {
    /// offset 视图滚动距离
    func scrollViewDidScrolling(offset: CGFloat)
}

2、UI布局

/// UI布局
    private func layoutUI() {
        addSubview(scrollView)
        scrollView.frame = self.bounds
        
        if let dataSource = dataSource {
            headerView = dataSource.LGNestViewHeaderView()
            sectionHeaderView = dataSource.LGNestViewSectionHeaderView()
            
            headerHeight = dataSource.LGNestViewHeaderViewHeight()
            /// 防止headerView高度过小 - 高度小于导航栏高度就没有意义这样做了
            if headerHeight < kLGStatusHeight + kLGNavigationBarHeight {
                headerHeight = kLGStatusHeight + kLGNavigationBarHeight
            }
            sectionHeaderHeight = dataSource.LGNestViewSectionHeaderViewHeight()
            
            scrollView.addSubview(headerView!)
            scrollView.addSubview(sectionHeaderView!)
            scrollView.addSubview(contentScrollView)
            
            headerView!.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: headerHeight)
            contentScrollView.frame = CGRect(x: 0, y: headerHeight + sectionHeaderHeight, width: self.bounds.width, height: kLGScreenHeight)
            sectionHeaderView!.frame = CGRect(x: 0, y: headerHeight, width: self.bounds.width, height: sectionHeaderHeight)
            
            for (index, view) in dataSource.LGNestViewContentViews().enumerated() {
                contentScrollView.addSubview(view)
                view.delegate = self
                /// 设置子视图frame
                /// 高度 = 屏幕高度 - 导航栏高度 - 悬浮视图高度
                view.frame = CGRect(x: CGFloat(index) * self.bounds.width, y: 0, width: self.bounds.width, height: kLGScreenHeight - kLGStatusHeight - kLGNavigationBarHeight - sectionHeaderHeight)
            }
            
            /// 设置scrollViewcontentSize
            /// 屏幕高度 + header高度 - 导航栏高度
            scrollView.contentSize = CGSize(width: 0, height: self.bounds.height + headerHeight - kLGStatusHeight - kLGNavigationBarHeight)
            
            contentScrollView.contentSize = CGSize(width: CGFloat(dataSource.LGNestViewContentViews().count) * self.bounds.width, height: 0)
        }
    }

3、主视图与子视图联动处理

extension LGNestView: LGNestProtocol {
    
    /// 处理子scrollView滚动时,主scrollView联动
    /// - Parameters:
    ///   - scrollView: 滚动的scrollView
    ///   - position: 滚动方向
    ///   - offset: 滚动距离
    func scrollViewDidScroll(scrollView: UIScrollView, position: LGScrollPosition, offset: CGFloat) {
        var scrollViewY = self.scrollView.contentOffset.y
        if position == .down {
            scrollViewY -= offset
        } else {
            scrollViewY += offset
        }
        
        if scrollViewY > headerHeight - kLGStatusHeight - kLGNavigationBarHeight {
            scrollViewY = headerHeight - kLGStatusHeight - kLGNavigationBarHeight
        } else if scrollViewY <= 0 {
            scrollViewY = 0
        }
        
        self.scrollView.setContentOffset(CGPoint(x: 0, y: scrollViewY), animated: false)
    }
}

注:这里处理的效果是子视图向上或向下滚动时悬浮视图立即响应,比如:悬浮窗口已经在最上方时,子视图只要向下滚动悬浮窗口也会立即向下滚动,而不是等子视图的contentOffset.y为0时才向下滚动。

4、处理悬浮视图
悬浮视图的处理放在主视图代理方法scrollViewDidScroll中,并且将滚动的偏移量contentOffset.y回调给控制器,方便处理导航栏隐藏和显示等。

extension LGNestView: UIScrollViewDelegate {
    
    /// 设置悬浮视图坐标
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let scrollViewY = self.scrollView.contentOffset.y
        var sectionY: CGFloat = sectionHeaderView?.frame.origin.y ?? headerHeight
        
        if scrollViewY >= headerHeight - kLGStatusHeight - kLGNavigationBarHeight {
            sectionY = scrollViewY + kLGStatusHeight + kLGNavigationBarHeight
        } else {
            sectionY = headerHeight
        }
        sectionHeaderView?.frame = CGRect(x: 0, y: sectionY, width: self.bounds.width, height: sectionHeaderHeight)
        
        delegate?.scrollViewDidScrolling(offset: scrollViewY)
    }
}
子视图:

1、使用代理方法回调子视图滚动处理结果

enum LGScrollPosition {
    case up
    case down
}

protocol LGNestProtocol: NSObjectProtocol {
    /// 视图滚动回调
    /// - Parameters:
    ///   - scrollVIew: 滚动的scrollView
    ///   - position: 滚动方向
    ///   - offset: 滚动距离
    func scrollViewDidScroll(scrollView: UIScrollView, position: LGScrollPosition, offset: CGFloat)
}

2、定义一个基类处理所有子视图滚动事件

class LGNestContentView: UIView {
    weak var delegate: LGNestProtocol?
    /// scrollView的contentOffset.y最后的值 - 用于判断滚动方向
    private var lastOffsetY: CGFloat = 0
    /// scrollView最后的滚动方向 - 用于处理减速回弹效果
    private var lastPosition: LGScrollPosition = .up
    
    /// 处理scrollView滚动 - 子类需要在scrollView的delegate方法"scrollViewDidScroll"中调用该方法
    fileprivate func viewDidScrolling(scrollView: UIScrollView) {
        
        /// y轴偏移量
        let y = scrollView.contentOffset.y
        
        /// 获取最后滚动的方向
        var position: LGScrollPosition = lastPosition
        /// 当手指离开屏幕后及回弹效果发生时,防止方向改变
        if scrollView.isDragging && !scrollView.isDecelerating {
            if y < lastOffsetY {
                position = .down
            } else {
                position = .up
            }
        }
        
        lastPosition = position
        
        /// 滚动距离
        let distance = CGFloat(fabsf(fabsf(Float(y)) - fabsf(Float(lastOffsetY))))
        /// 代理传值
        delegate?.scrollViewDidScroll(scrollView: scrollView, position: position, offset: distance)
        lastOffsetY = y
    }
}

3、子视图只需要继承自这个基类,并在scrollView、tableView、collectionView代理方法scrollViewDidScroll中调用父类的viewDidScrolling方法即可。

class FirstView: LGNestContentView {
}
extension FirstView: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        viewDidScrolling(scrollView: scrollView)
    }
}

结语:

为了做这个效果,网上也看了别人的一些方案,写的都比较复杂,当然效果也很好。这个做的比较简陋除了高度计算繁琐一些,其他的都没啥难度,别人的都是使用collectionView来做内容视图,这样可以复用子视图,而我没有想到,写完了才想到这个点,so sad~
就这样吧,下雨天打孩子 —— 反正闲着也是闲着,就当练练手了。

Just do IT~

你可能感兴趣的:(iOS嵌套scrollView联动加顶部悬浮效果)