iOS_NestedScrollView(嵌套ScrollView)
手势协议
首先需要了解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、初始化视图
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
由于UIScrollView
的contentOffset
的精确度问题,所以在计算或判等时需要注意了。
这里有两种实现方案:
- 1、
contentInset.top
取整 - 2、使用
FLT_EPSILON
(double
用DBL_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)相关推荐
- Android ScrollView嵌套ScrollView滚动的问题解决办法
引用:http://mengsina.iteye.com/blog/1707464 http://fenglog.com/article.asp?id=449 Android ScrollView嵌套 ...
- 不需嵌套ScrollView就可以滚动TextView的方法
不需嵌套ScrollView就可以滚动TextView的方法 需要同时设置如下2个属性,这样就不需要使用传统嵌套ScrollView方法,即使文本内容特别长 textView.movementMeth ...
- 在android中ScrollView嵌套ScrollView解决方案
文章转载自:http://www.jb51.net/article/33054.htm 大家好,众所周知,android里两个相同方向的ScrollView是不能嵌套的,那要是有这样的需求怎么办,接下 ...
- [Android实例] [版主原创]ScrollView嵌套ScrollView
大家好,众所周知,android 里两个相同方向的ScrollView是不能嵌套的,那要是有这样的需求怎么办?(这个需求一般都是不懂android的人提出来的) 难道就真的不能嵌套吗? 当然可以,只要 ...
- recylerview嵌套scrollview卡顿
现象: 一个界面有多个RecyclerView以及其他一些内容,这时要上下滚动就会使用外面嵌套一个ScrollView,虽然我没有遇到像ScrollView嵌套ListView时那样只显示部分,剩余不 ...
- ScrollView嵌套ScrollView
原博客地址: http://www.eoeandroid.com/thread-240709-1-1.html 大家好,众所周知,android 里两个相同方向的ScrollView是不能嵌套的,那要 ...
- [Android] (在ScrollView里嵌套view)重叠view里面的onTouchEvent的调用方法
在我前面的自定义裁剪窗口的代码中,我把裁剪的view放在了大的scrollview里,这样就出现了程序只能触发scrollview,无法操作我的裁剪窗口.所以我加了那篇博客下面最后两段代码.其实我遇到 ...
- android之ScrollView里嵌套ListView(转)
hi,大家好,研究完ScrollView嵌套ScrollView之后,本人突然又想研究ScrollView里嵌套ListView了. 如果还不知道ScrollView嵌套ScrollView是怎么实现 ...
- iOS uiscrollView 嵌套 问题 的解决
苹果官方文档里面提过,最好不要嵌套scrollView,特别提过UITableView和UIWebView,因为在滑动时,无法知道到底是希望superScrollView滑动还是subScrollVi ...
最新文章
- Math对象及相关方法
- 奇奇seo优化软件_seo优化软件如何选择
- Android Keystore/keymaster的错误码
- 亏损63亿,美图真能“美”到上市?
- 【Cannot convert from [[B] to】 @RabbitListener 反序列化报错
- Coding:就地合并两个排序数组
- 图解Android - Android GUI 系统 (2) - 窗口管理 (View, Canvas, Window Manager)
- tcl linux 刷 安卓系统,安卓用户看过来—手把手教你刷第三方系统
- CrossOver 12 发布,Windows 模拟器
- fat linux 链接,FAT格式磁盘镜像制作方法
- 项目团队管理 Atitit 职位的自动分配草案 attilax总结
- 【博主推荐】html好看的个人简历网页版(附源码)
- linux 运行lammps,lammps linux运行
- linux系统配置Vim命令,怎么在LINUX操作系统中安装和配置VIM?
- 专访李智慧:架构是最高层次的规划和难以改变的决定
- android谷歌人脸识别,谷歌发布Android 4.0系统 支持人脸识别功能
- Transflow:Quake 是如何构建以 DSL 为核心的低代码系统?
- 游戏运营的十二大组成
- git log查看日志中文乱码的解决方法,绝对好用2021
- Android沙箱自动化安全产品
热门文章
- 实现信用卡用户定时还款功能
- 用eclipse编写html页面,简述Eclipse中的CSS编辑器使用
- 将十进制转换为8进制并输出
- 3D人脸重建之DECA
- Java学习记录(Day4)
- Counting Objects in C++
- 计算机职业素质选修课,化学专业学生教师职业素质拓展选修课课程设置体系
- 一、PyQt基础知识
- python __slots__ 详解(上篇)
- 俄罗斯计算机正博士,科学网—呃弱弱地说:俄罗斯正博士那算博士,咱们?嘿嘿都烂。 - 陈楷翰的博文...