内容简介:BannerHoverView - 解耦 TableView Header 实现悬停
邻近毕业,在完成毕设及论文之余,在帮助老师的创业公司写一个体育类 App。在其中遇到了这么一个需求,如下动图所示:
其实在很多的 App 中都需要这样的样式,尤其在个人设置页面中需要让个人信息的 Banner 视图部分悬停在顶部作为一个 Navigation Bar 的占位从而合理的展示页面。如何流畅的展现这个滑动过程呢?这篇博文为解决该布局和页面需求做出一个实验性的探索。
悬停效果
对于悬停的第一反应,自然是想起了 TableView 的 Header 悬停。但是如果采取 Header 的部分视图悬停,在实现起来难度就稍微大一点(使 Section 的最后一个 Cell 与 Header 一起编写逻辑,给用户部分悬停的错觉即可)。
再来说一些状态的回调。我们希望在滑动过程中拿出这几种状态来进行处理,滑动到顶部状态、底部状态以及在滑动过程中的状态。
在 BannerHoverView 中,为了实现多种状态的识别我使用了 KVO 来对滑动的 offset 参数进行观察,从而得到特定时刻的滑动状态。而对于悬停的实现,来通过计算来确定最新的 frame 即可。下面来具体说明实现方法。
BannerHoverView 实现
先来看一下参数属性:
static private let eps: CGFloat = 1e-6 public var headerScrollView: UIScrollView! public var top: CGFloat = 0 public fileprivate(set) var bottom: CGFloat = 0 public fileprivate(set) var isTop: Bool = false public fileprivate(set) var isBottom: Bool = true fileprivate var completeBlock: ((BannerHoverView) -> Void)? fileprivate var startBlock: ((BannerHoverView) -> Void)? fileprivate var scrollBlock: ((BannerHoverView, CGFloat) -> Void)?
- eps :用于 double 类型判等方法。
- headerScrollView :这是一个引用传递属性,用于将 View Controller 中的 Scroll View 传递进来。
- top :悬停高度。
- bottom :Banner 默认状态下总高度。
- isTop :判断 Banner 是否为顶部状态。
- isBottom :判断 Banner 是否为底部状态。
- completeBlock :Banner 到达底部时候的回调闭包。
- startBlock :Banner 到底顶部时候的回调闭包。
- scrollBlock :滑动状态中的回调。
CGFloat
参数为滑动进度,范围为 [0, 1] 闭区间。
这里使用 eps 的原因是因为 double 的精度。不知道的可以感受一下下图出现的原因。在四则运算中,加减法对精度的影响较小,而乘法对精度的影响更大,除法最大。在 BannerHoverView 中因为关系到 alpha 值的改变,所以还是尽量保证精度问题。
willMove 方法
public override func willMove(toWindow newWindow: UIWindow?) { super.willMove(toWindow: newWindow) headerScrollView.contentInset = UIEdgeInsets.init(top: bottom, left: 0, bottom: 0, right: 0) // 感谢 @Josscii 的 PR headerScrollView.scrollIndicatorInsets = UIEdgeInsets.init(top: bottom, left: 0, bottom: 0, right: 0) }
willMove
这个方法即为 willMoveToWindow
。在 View Controller 的 viewWillAppear
周期方法中会调用其子视图中该方法。而在 BannerHoverView 中,这个时机十分适合设置 Table View 的上下偏移,以及 Indicator 的上下偏移。由于其 bottom 属性已经在初始化的时候确定。
用 KVO 对 Offset 进行监听的核心部分
// MARK: - KVO override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if let new = change?[NSKeyValueChangeKey.newKey] { let point = (new as! NSValue).cgPointValue updateSubViewsWithScrollOffset(newOffset: point) } }
在 observeValue
方法中,我需要获得的是 Table View 的 contentOffset
属性,拿到的是一个 Point 对象。然后根据这个新的坐标,来更新视图操作,即调用 updateSubViewsWithScrollOffset
方法。
fileprivate func updateSubViewsWithScrollOffset(newOffset: CGPoint) { var newOffset = newOffset // 取出额外滚动区域的顶部位置,然后取反 let startChangeOffset = -headerScrollView.contentInset.top // 计算滑动点,通过 top 和 startChangeOffset 来确定滑动区域 newOffset = CGPoint.init(x: newOffset.x, y: newOffset.y < startChangeOffset ? startChangeOffset : min(newOffset.y, -top)) // 根据滑动点范围确定 frame 的 y 坐标 let newY = -newOffset.y - bottom // 根据滑动确定 frame 新值 frame = CGRect.init(x: 0, y: newY, width: frame.size.width, height: frame.size.height) // 计算总滑动距离 let distance = -top - startChangeOffset // 计算滑动距离百分比 let percent = 1 - (newOffset.y - startChangeOffset) / distance // 回调处理部分,更新状态 if 1.0 - percent > BannerHoverView.eps && percent - 0.0 > BannerHoverView.eps { isBottom = false isTop = false } else if isBottom == false && isTop == false { if 1.0 - percent < BannerHoverView.eps { isTop = true if let topAction = completeBlock { topAction(self) } } else if percent - 0.0 < BannerHoverView.eps { isBottom = true if let bottomAction = startBlock { bottomAction(self) } } } // 调用滑动时期闭包方法 if let scrollAction = scrollBlock { scrollAction(self, percent) } }
在更新滑动的操作中,无非就是在做两件事情:
- 判断滑动坐标是否越界(bottom 和 top 参数进行限制)。
- 判断滑动状态以触发不同状态的回调方法。
这大概 30 行左右的代码就是 BannerHoverView 的核心部分,感觉较为精简。如果有更好的实现方法,欢迎 PR。
在 View Controller 初始化 BannerHoverView
你只需要三步就可以初始化 BannerHoverView 并使用:
- 对其中的
headerScrollView
进行关联:
// TableView Initial tableView = UITableView.init(frame: view.bounds, style: .grouped) tableView.dataSource = self tableView.delegate = self // BannerHoverView Initial bannerHoverView = SampleView.init(frame: CGRect.init(x: 0, y: 0, width: view.frame.size.width, height: 280)) // Hover Height(Remaining part when BannerHoverView arrived at the top position) bannerHoverView.top = 65 // Scroll Property Setting bannerHoverView.headerScrollView = tableView // Add Observer tableView.addObserver(bannerHoverView, forKeyPath: "contentOffset", options: NSKeyValueObservingOptions.new, context: nil) view.addSubview(tableView) view.addSubview(bannerHoverView)
- 在 deinit 中删除 KVO 监听
deinit { tableView.removeObserver(bannerHoverView, forKeyPath: "contentOffset") }
- 继承
BannerHoverView
来定制悬停部分视图:
class SampleView: BannerHoverView { override init(frame: CGRect) { super.init(frame: frame) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } self.setScrollAction { (view, offset) in // offset - distance percent } self.setTopAction { (view) in // scroll top callback } self.setBottomAction { (view) in // scroll bottom callback } }
以上便是 BannerHoverView 的实现思路,希望这个思路可以给大家启发,也希望得到更好的解决办法,在讨论中学习最优雅的实现方式。
以上所述就是小编给大家介绍的《BannerHoverView - 解耦 TableView Header 实现悬停》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- BannerHoverView - 解耦 TableView Header 实现悬停
- 鼠标悬停动画效果
- 大图背景悬停导航菜单
- 图片悬停“滑动打开”动画效果
- 图片不同方向悬停显示不同文字
- 按钮悬停边框和背景动画集合
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First Design Patterns
Elisabeth Freeman、Eric Freeman、Bert Bates、Kathy Sierra、Elisabeth Robson / O'Reilly Media / 2004-11-1 / USD 49.99
You're not alone. At any given moment, somewhere in the world someone struggles with the same software design problems you have. You know you don't want to reinvent the wheel (or worse, a flat tire),......一起来看看 《Head First Design Patterns》 这本书的介绍吧!