手势协议
首先需要了解UIGestureRecognizerDelegate
协议的这个方法:
/// 是否同时相应这俩手势,默认返回 false
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
当底部scrollView
返回true
时,添加在它上面的scrollView
滑动时,它也可以滑动了。
这时候两个scrollView
都会滑动,我们可以在滑动回调里,根据当前的情况进行处理,实现想要的滑动规则了。
滑动规则制定
Tips:规则一定要提前确认好。
实现抽屉效果如下:
下拉:内部列表
拉到最顶部了,才放大headerView
上拉:先把headerView
缩到最小,再上滑内部列表
实现
1、层级关系
mainScrollView
:添加在vc.view
上,铺满。其顶部内边距contentInset.top
等于header
的最大高度
-最小高度
即 可滑动的高度。tabContainerView
:添加在mainScrollView
上,但其originY
是headerView
的最小高度。headerView
: 添加在vc.view
上,置顶,其高度根据mainScrollView.contentOffset.y
计算出来,使其正好贴在tabContainerView
上。
注:这样布局的原因是:不需要频繁的修改
headerView
和tabContainerView
的frame
,只需要修改他们的高度就行。卡顿效果能明显减少。
2、初始化视图
代码语言:swift复制private lazy var mainScrollView: MOMultiResponseScrollView = {
let scroll = MOMultiResponseScrollView(frame: .zero)
scroll.delegate = self
scroll.bounces = false
scroll.backgroundColor = .blue
return scroll
}()
private lazy var headerView: UIView = {
let view = UIView(frame: .zero)
view.backgroundColor = .red
return view
}()
private lazy var tabsContainerCtl: MOMultiTabContainerViewController = {
let ctl = MOMultiTabContainerViewController(nibName: nil, bundle: nil)
ctl.view.backgroundColor = .cyan
return ctl
}()
MOMultiResponseScrollView
内部实现了UIGestureRecognizerDelegate
,允许俩手势同时相应func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true }undefinedMOMultiTabContainerViewController
内部是一个scrollView
,添加多个subScrollView
,结构如下:(详情可见MOMultiTabContainerViewController.swift)
3、添加视图
代码语言:swift复制override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.mainScrollView)
self.mainScrollView.addSubview(self.tabsContainerCtl.view)
self.view.addSubview(self.headerView)
}
4、布局
代码语言:swift复制override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let viewSize = self.view.bounds.size
let safeInset = self.view.safeAreaInsets
let containerWidth = viewSize.width - safeInset.left - safeInset.right
let containerHeight = viewSize.height - safeInset.top - safeInset.bottom
let mainScrollView = self.mainScrollView
let headerView = self.headerView
let tabsContainerView = self.tabsContainerCtl.view
/// 铺满
mainScrollView.frame = CGRect(x: safeInset.left,
y: safeInset.top,
width: containerWidth,
height: containerHeight)
mainScrollView.contentSize = CGSize(width: containerWidth,
height: containerHeight)
/// 内边距为可滑动值
let scrollTopInset = headerViewMaxHeight - headerViewMinHeight
mainScrollView.contentInset = UIEdgeInsets(top: scrollTopInset,
left: 0.0,
bottom: 0.0,
right: 0.0)
/// 高度根据偏移算出
let headerHeight = headerViewMinHeight abs(mainScrollView.contentOffset.y)
headerView.frame = CGRect(x: safeInset.left,
y: safeInset.top,
width: containerWidth,
height: headerHeight)
/// 高度等于剩下的范围
tabsContainerView?.frame = CGRect(x: 0.0,
y: headerViewMinHeight,
width: containerWidth,
height: containerHeight - headerHeight)
}
5、传递滑动回调
将所有滑动回调都交由MOSubScrollExecutor
处理:(把嵌套滑动规则集中在一个文件里,方便管理和复用)
// MARK: - Private Methods - 主 ScrollView 的回调事件
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.scrollExecutor.mainScrollViewWillBeginDragging(scrollView)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.scrollExecutor.mainScrollViewDidScroll(scrollView)
}
代码语言:swift复制private lazy var tabsContainerCtl: MOMultiTabContainerViewController = {
let ctl = MOMultiTabContainerViewController(nibName: nil, bundle: nil)
/// 内部 ScrollView 的回调事件
ctl.willBeginDragging = { [weak self] (scrollView: UIScrollView) in
self?.scrollExecutor.subScrollWillBeginDragging(scrollView)
}
ctl.didScroll = { [weak self] (scrollView: UIScrollView) in
self?.scrollExecutor.subScrollDidScroll(scrollView)
}
ctl.view.backgroundColor = .cyan
return ctl
}()
6、处理滑动回调
6.1、标记属性:
代码语言:swift复制/// 用于判断其最大最小状态
private var mainScrollView: UIScrollView?
/// 记录拖拽前的偏移,用于不可滑动状态时,重置偏移
private var mainScrollOffsetBeforeDragging: CGPoint = .zero
/// 是否处于可滑动状态
private var mainScrollEnable: Bool
/// 用于防重入
private var currentSubScrollView: UIScrollView?
/// 记录拖拽前的偏移,用于不可滑动状态时,重置偏移
private var subScrollViewPreOffset: CGPoint = .zero
6.2、helper
方法:
代码语言:swift复制/// 判断最大最小态:
func headerIsMinState() -> Bool {
return mainScrollView.contentOffset.y.isEqual(to: 0.0)
}
func headerIsMaxState() -> Bool {
return mainScrollView.contentInset.top.isEqual(to: abs(mainScrollView.contentOffset.y))
}
/// 重置偏移的方法:
/// 更新 scrollView 的 offset, 相同时跳过,防止极限情况死循环
private func updateScrollView(_ scrollView: UIScrollView, _ offset: CGPoint) {
if scrollView.contentOffset.equalTo(offset) {
return
}
scrollView.contentOffset = offset;
}
6.3、mainScrollView
的滑动回调:
代码语言:swift复制public func mainScrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.mainScrollView = scrollView
/// 记录拖拽前的偏移
self.mainScrollOffsetBeforeDragging = scrollView.contentOffset
}
public func mainScrollViewDidScroll(_ scrollView: UIScrollView) {
if self.mainScrollEnable {
/// 需要重新布局,重新计算 headerView 和 containerView 的高度
/// 触发 MONestedScrollViewController 的 viewDidLayoutSubviews 方法
self.mainScrollSuperView?.setNeedsLayout()
return
}
/// 不可滑动时,重置偏移
self.updateScrollView(scrollView, self.mainScrollOffsetBeforeDragging)
}
6.4、subScrollView
的滑动回调:
代码语言:swift复制public func subScrollWillBeginDragging(_ scrollView: UIScrollView) {
/// 切换tab时重置标记位
if self.currentSubScrollView != nil &&
!self.currentSubScrollView!.isEqual(scrollView) {
self.mainScrollEnable = true
}
self.currentSubScrollView = scrollView
self.subScrollViewPreOffset = scrollView.contentOffset
}
public func subScrollDidScroll(_ scrollView: UIScrollView) {
/// 丢弃其他scrollView的回调(case: 刚拖拽完tabView,立马切换到webView,此时还会收到tabView的滑动回调)
if !scrollView.isEqual(self.currentSubScrollView) {
return
}
if scrollView.contentOffset.y.isEqual(to: self.subScrollViewPreOffset.y) {
return
}
let pullDown: Bool = scrollView.contentOffset.y < self.subScrollViewPreOffset.y
if pullDown {
self.handlePullDown(scrollView) /// 处理下拉
} else {
self.handlePullUp(scrollView) /// 处理上拉
}
}
这里也有用手势的速度来判断上拉 or 下拉的,但是在手离开后的减速滑动时速度就为0了,所以这里没有用velocity
。
6.5、处理subScrollView
下拉:
代码语言:swift复制/// 下拉: list 先拉到顶,再放大 headerView
func handlePullDown(_ scrollView: UIScrollView) {
/// 还没拉到顶 或 headerView已是最大状态,允许subScrollView滑动,不做处理
if scrollView.contentOffset.y > 0 ||
self.headerIsMaxState() {
self.mainScrollEnable = false
self.subScrollViewPreOffset = scrollView.contentOffset
} else {
/// 拉到顶部了 且 播放器需要放大
self.mainScrollEnable = true
/// 重置偏移(放大player时,不需要下拉刷新效果)
self.updateScrollView(scrollView, .zero)
self.subScrollViewPreOffset = .zero
}
}
6.6、处理subScrollView
上拉:
代码语言:swift复制/// pullUp 上拉: 先缩小播放器,再拉 list
func handlePullUp(_ scrollView: UIScrollView) {
/// headerView 已是最小状态,允许subScrollView滑动,不做处理
if self.headerIsMinState() {
self.mainScrollEnable = false
self.subScrollViewPreOffset = scrollView.contentOffset
return
}
self.mainScrollEnable = true
if scrollView.contentOffset.y <= 0 { /// 忽略下拉刷新的回弹(否则死循环)
return
}
print("headerView缩小时,重置subScrollView偏移")
self.updateScrollView(scrollView, self.subScrollViewPreOffset)
}
CGFloat判等
都知道1.1 * 1.1 = 1.21
,但在代码里确不一定:
let first = 1.1
let second = 1.1
let result = first * second
let floatEqual = result == 1.21
print("(first) * (second) = (result) is (floatEqual)")
// log:
1.1 * 1.1 = 1.2100000000000002 is false
由于UIScrollView
的contentOffset
的精确度问题,所以在计算或判等时需要注意了。
这里有两种实现方案:
- 1、
contentInset.top
取整let firstNum = NSDecimalNumber(string: "1.1") let secondNum = NSDecimalNumber(string: "1.1") let resultNum = firstNum.multiplying(by: secondNum) let numberEqual = resultNum.compare(NSDecimalNumber(string: "1.21")) == .orderedSame print("(firstNum) * (secondNum) = (resultNum) is (numberEqual)") // log: 1.1 * 1.1 = 1.21 is true - 2、使用`FLT_EPSILON`
```swift
let equal = fabs(streamRatio - QNBUALiveShowPlayerDefaultAspectRatio) < FLT_EPSILON;
```
- 3、`contentOffset.y` 判等时 使用 `NSDecimalNumber`
github demo
参考:
Strange problem comparing floats in objective-C