先上效果图:
原理:
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~
就这样吧,下雨天打孩子 —— 反正闲着也是闲着,就当练练手了。