BannerHoverView - 解耦 TableView Header 实现悬停

栏目: IOS · 发布时间: 6年前

内容简介:BannerHoverView - 解耦 TableView Header 实现悬停

邻近毕业,在完成毕设及论文之余,在帮助老师的创业公司写一个体育类 App。在其中遇到了这么一个需求,如下动图所示:

BannerHoverView - 解耦 TableView Header 实现悬停

其实在很多的 App 中都需要这样的样式,尤其在个人设置页面中需要让个人信息的 Banner 视图部分悬停在顶部作为一个 Navigation Bar 的占位从而合理的展示页面。如何流畅的展现这个滑动过程呢?这篇博文为解决该布局和页面需求做出一个实验性的探索。

悬停效果

对于悬停的第一反应,自然是想起了 TableView 的 Header 悬停。但是如果采取 Header 的部分视图悬停,在实现起来难度就稍微大一点(使 Section 的最后一个 Cell 与 Header 一起编写逻辑,给用户部分悬停的错觉即可)。

再来说一些状态的回调。我们希望在滑动过程中拿出这几种状态来进行处理,滑动到顶部状态、底部状态以及在滑动过程中的状态。

BannerHoverView 中,为了实现多种状态的识别我使用了 KVO 来对滑动的 offset 参数进行观察,从而得到特定时刻的滑动状态。而对于悬停的实现,来通过计算来确定最新的 frame 即可。下面来具体说明实现方法。

BannerHoverView - 解耦 TableView Header 实现悬停

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 值的改变,所以还是尽量保证精度问题。

BannerHoverView - 解耦 TableView Header 实现悬停

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)
   }
}

在更新滑动的操作中,无非就是在做两件事情:

  1. 判断滑动坐标是否越界(bottom 和 top 参数进行限制)。
  2. 判断滑动状态以触发不同状态的回调方法。

这大概 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 的实现思路,希望这个思路可以给大家启发,也希望得到更好的解决办法,在讨论中学习最优雅的实现方式。

Github: https://github.com/Desgard/BannerHoverView


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

科学的极致:漫谈人工智能

科学的极致:漫谈人工智能

集智俱乐部 / 人民邮电出版社 / 2015-7 / 49.00元

集智俱乐部是一个从事学术研究、享受科学乐趣的探索者组成的团体,倡导以平等开放的态度、科学实证的精神进行跨学科的研究与交流,力图搭建一个中国的“没有围墙的研究所”。这些令人崇敬的、充满激情与梦想的集智俱乐部成员将带你了解图灵机模型、冯•诺依曼计算机体系结构、怪圈与哥德尔定理、通用人工智能、深度学习、人类计算与自然语言处理,与你一起展开一场令人热血沸腾的科学之旅。一起来看看 《科学的极致:漫谈人工智能》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具