手势协议

首先需要了解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上,但其originYheaderView的最小高度。
  • headerView: 添加在vc.view上,置顶,其高度根据mainScrollView.contentOffset.y计算出来,使其正好贴在tabContainerView上。

注:这样布局的原因是:不需要频繁的修改headerViewtabContainerViewframe,只需要修改他们的高度就行。卡顿效果能明显减少。


2、初始化视图

private lazy var mainScrollView: MOMultiResponseScrollView = {let scroll = MOMultiResponseScrollView(frame: .zero)scroll.delegate = selfscroll.bounces = falsescroll.backgroundColor = .bluereturn scroll
}()private lazy var headerView: UIView = {let view = UIView(frame: .zero)view.backgroundColor = .redreturn view
}()private lazy var tabsContainerCtl: MOMultiTabContainerViewController = {let ctl = MOMultiTabContainerViewController(nibName: nil, bundle: nil)ctl.view.backgroundColor = .cyanreturn ctl
}()

  • MOMultiResponseScrollView内部实现了UIGestureRecognizerDelegate,允许俩手势同时相应
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {return true
}

  • MOMultiTabContainerViewController内部是一个scrollView,添加多个subScrollView,结构如下:(详情可见MOMultiTabContainerViewController.swift)


3、添加视图

override func viewDidLoad() {super.viewDidLoad()self.view.addSubview(self.mainScrollView)self.mainScrollView.addSubview(self.tabsContainerCtl.view)self.view.addSubview(self.headerView)
}

4、布局

override func viewDidLayoutSubviews() {super.viewDidLayoutSubviews()let viewSize = self.view.bounds.sizelet safeInset = self.view.safeAreaInsetslet containerWidth = viewSize.width - safeInset.left - safeInset.rightlet containerHeight = viewSize.height - safeInset.top - safeInset.bottomlet mainScrollView = self.mainScrollViewlet headerView = self.headerViewlet 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 - headerViewMinHeightmainScrollView.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)
}
private lazy var tabsContainerCtl: MOMultiTabContainerViewController = {let ctl = MOMultiTabContainerViewController(nibName: nil, bundle: nil)/// 内部 ScrollView 的回调事件ctl.willBeginDragging = { [weak self] (scrollView: UIScrollView) inself?.scrollExecutor.subScrollWillBeginDragging(scrollView)}ctl.didScroll = { [weak self] (scrollView: UIScrollView) inself?.scrollExecutor.subScrollDidScroll(scrollView)}ctl.view.backgroundColor = .cyanreturn ctl
}()

6、处理滑动回调

6.1、标记属性:

/// 用于判断其最大最小状态
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方法:

/// 判断最大最小态:
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的滑动回调:

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的滑动回调:

public func subScrollWillBeginDragging(_ scrollView: UIScrollView) {/// 切换tab时重置标记位if self.currentSubScrollView != nil &&!self.currentSubScrollView!.isEqual(scrollView) {self.mainScrollEnable = true}self.currentSubScrollView = scrollViewself.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.yif pullDown {self.handlePullDown(scrollView) /// 处理下拉} else {self.handlePullUp(scrollView)   /// 处理上拉}
}

这里也有用手势的速度来判断上拉 or 下拉的,但是在手离开后的减速滑动时速度就为0了,所以这里没有用velocity


6.5、处理subScrollView下拉:

/// 下拉: list 先拉到顶,再放大 headerView
func handlePullDown(_ scrollView: UIScrollView) {    /// 还没拉到顶 或 headerView已是最大状态,允许subScrollView滑动,不做处理if scrollView.contentOffset.y > 0 ||self.headerIsMaxState() {self.mainScrollEnable = falseself.subScrollViewPreOffset = scrollView.contentOffset} else {/// 拉到顶部了 且 播放器需要放大self.mainScrollEnable = true/// 重置偏移(放大player时,不需要下拉刷新效果)self.updateScrollView(scrollView, .zero)self.subScrollViewPreOffset = .zero}
}

6.6、处理subScrollView上拉:

/// pullUp 上拉: 先缩小播放器,再拉 list
func handlePullUp(_ scrollView: UIScrollView) {    /// headerView 已是最小状态,允许subScrollView滑动,不做处理if self.headerIsMinState() {self.mainScrollEnable = falseself.subScrollViewPreOffset = scrollView.contentOffsetreturn}self.mainScrollEnable = trueif 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

由于UIScrollViewcontentOffset的精确度问题,所以在计算或判等时需要注意了。

这里有两种实现方案:

  • 1、contentInset.top 取整
  • 2、使用FLT_EPSILONdoubleDBL_EPSILON
let equal = fabs(streamRatio - QNBUALiveShowPlayerDefaultAspectRatio) < FLT_EPSILON;
  • 3、contentOffset.y 判等时 使用 NSDecimalNumber
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

github demo


参考:

Strange problem comparing floats in objective-C

iOS_NestedScrollView(嵌套ScrollView)相关推荐

  1. Android ScrollView嵌套ScrollView滚动的问题解决办法

    引用:http://mengsina.iteye.com/blog/1707464 http://fenglog.com/article.asp?id=449 Android ScrollView嵌套 ...

  2. 不需嵌套ScrollView就可以滚动TextView的方法

    不需嵌套ScrollView就可以滚动TextView的方法 需要同时设置如下2个属性,这样就不需要使用传统嵌套ScrollView方法,即使文本内容特别长 textView.movementMeth ...

  3. 在android中ScrollView嵌套ScrollView解决方案

    文章转载自:http://www.jb51.net/article/33054.htm 大家好,众所周知,android里两个相同方向的ScrollView是不能嵌套的,那要是有这样的需求怎么办,接下 ...

  4. [Android实例] [版主原创]ScrollView嵌套ScrollView

    大家好,众所周知,android 里两个相同方向的ScrollView是不能嵌套的,那要是有这样的需求怎么办?(这个需求一般都是不懂android的人提出来的) 难道就真的不能嵌套吗? 当然可以,只要 ...

  5. recylerview嵌套scrollview卡顿

    现象: 一个界面有多个RecyclerView以及其他一些内容,这时要上下滚动就会使用外面嵌套一个ScrollView,虽然我没有遇到像ScrollView嵌套ListView时那样只显示部分,剩余不 ...

  6. ScrollView嵌套ScrollView

    原博客地址: http://www.eoeandroid.com/thread-240709-1-1.html 大家好,众所周知,android 里两个相同方向的ScrollView是不能嵌套的,那要 ...

  7. [Android] (在ScrollView里嵌套view)重叠view里面的onTouchEvent的调用方法

    在我前面的自定义裁剪窗口的代码中,我把裁剪的view放在了大的scrollview里,这样就出现了程序只能触发scrollview,无法操作我的裁剪窗口.所以我加了那篇博客下面最后两段代码.其实我遇到 ...

  8. android之ScrollView里嵌套ListView(转)

    hi,大家好,研究完ScrollView嵌套ScrollView之后,本人突然又想研究ScrollView里嵌套ListView了. 如果还不知道ScrollView嵌套ScrollView是怎么实现 ...

  9. iOS uiscrollView 嵌套 问题 的解决

    苹果官方文档里面提过,最好不要嵌套scrollView,特别提过UITableView和UIWebView,因为在滑动时,无法知道到底是希望superScrollView滑动还是subScrollVi ...

最新文章

  1. Math对象及相关方法
  2. 奇奇seo优化软件_seo优化软件如何选择
  3. Android Keystore/keymaster的错误码
  4. 亏损63亿,美图真能“美”到上市?
  5. 【Cannot convert from [[B] to】 @RabbitListener 反序列化报错
  6. Coding:就地合并两个排序数组
  7. 图解Android - Android GUI 系统 (2) - 窗口管理 (View, Canvas, Window Manager)
  8. tcl linux 刷 安卓系统,安卓用户看过来—手把手教你刷第三方系统
  9. CrossOver 12 发布,Windows 模拟器
  10. fat linux 链接,FAT格式磁盘镜像制作方法
  11. 项目团队管理 Atitit 职位的自动分配草案 attilax总结
  12. 【博主推荐】html好看的个人简历网页版(附源码)
  13. linux 运行lammps,lammps linux运行
  14. linux系统配置Vim命令,怎么在LINUX操作系统中安装和配置VIM?
  15. 专访李智慧:架构是最高层次的规划和难以改变的决定
  16. android谷歌人脸识别,谷歌发布Android 4.0系统 支持人脸识别功能
  17. Transflow:Quake 是如何构建以 DSL 为核心的低代码系统?
  18. 游戏运营的十二大组成
  19. git log查看日志中文乱码的解决方法,绝对好用2021
  20. Android沙箱自动化安全产品

热门文章

  1. 实现信用卡用户定时还款功能
  2. 用eclipse编写html页面,简述Eclipse中的CSS编辑器使用
  3. 将十进制转换为8进制并输出
  4. 3D人脸重建之DECA
  5. Java学习记录(Day4)
  6. Counting Objects in C++
  7. 计算机职业素质选修课,化学专业学生教师职业素质拓展选修课课程设置体系
  8. 一、PyQt基础知识
  9. python __slots__ 详解(上篇)
  10. 俄罗斯计算机正博士,科学网—呃弱弱地说:俄罗斯正博士那算博士,咱们?嘿嘿都烂。 - 陈楷翰的博文...