OLEContainerScrollView源码解析(二):layoutSubviews三步布局算法深度拆解
【免费下载链接】OLEContainerScrollViewA UIScrollView subclass that intelligently handles multiple child scroll views and does not interfere with UIKitʼs cell reuse functionality.项目地址: https://gitcode.com/gh_mirrors/ol/OLEContainerScrollView
OLEContainerScrollView 是一个经典的 UIScrollView 子类,能智能管理多个嵌套子滚动视图(UITableView、UICollectionView、普通 UIView),同时不破坏 UIKit 的 Cell 复用机制。上一篇文章介绍了它的整体架构,本篇深入核心方法layoutSubviews,完整拆解它"三步布局算法"的每一步——看懂这三步,你就掌握了容器式滚动布局的全部精髓。
一、为什么需要容器滚动视图?先明确问题
在 iPhone 上"多个列表纵向拼接"是个高频需求:顶部一个广告列表,中间一个商品列表,底部一个相关推荐列表。如果直接把三个 UITableView 放进一个大 UIScrollView,会遇到两个致命问题:
- 子列表高度被撑开:所有 Cell 一次性创建,内存爆炸;
- 滚动冲突:手势在父子两个滚动视图之间"抢方向",体验割裂。
OLEContainerScrollView 的思路是:让子滚动视图的 frame 随容器滚动动态伸缩,让屏幕外不可见的部分高度收缩为 0。这样子列表的可见高度始终不超过屏幕高度,Cell 复用机制得以完整保留,而用户感知到的却是"一个统一的大列表"。
🎯 核心源码就在 [这里] 的布局方法中:OLEContainerScrollView/OLEContainerScrollView.m第 160–257 行的layoutSubviews。
二、布局算法的前提:谁触发了重新布局?
三步算法是"结果",触发器同样关键。容器对每个子视图建立了 KVO 监听(见OLEContainerScrollView/OLEContainerScrollView.m第 94–105 行):
- 子视图是 UIScrollView 或其子类(通过
scrollView协议识别,协议定义在OLEContainerScrollView/OLEContainerScrollView.h)→ 监听其contentSize; - 普通视图 → 监听其frame和bounds。
任何一项变化都会调用setNeedsLayout+layoutIfNeeded,立即重算布局。这就是"子列表数据一变,容器自动调整总高度"的原理,全程无轮询、无定时器。
子视图的排列顺序由私有数组subviewsInLayoutOrder维护,它在子视图被加入contentView时同步更新(didAddSubviewToContainer:,第 78–108 行)。这个"中介层"正是OLEContainerScrollView/OLEContainerScrollViewContentView.m的职责:它重写了didAddSubview:/willRemoveSubview:,把增删事件转发给容器。
三、第一步:偏移量转译(Offset Translation)
self.contentView.frame = self.bounds; self.contentView.bounds = (CGRect){ self.contentOffset, self.contentView.bounds.size };这两行是整个算法的"地基"(第 168–169 行)。巧妙之处在于:
contentView.frame = self.bounds:contentView 永远和可视区等大、等大居中,它本身不需要做成整个内容那么高;- 把容器的
contentOffset直接写进contentView.bounds.origin:滚动容器时,相当于移动 contentView 的"窗口",而不是移动它的位置。
效果是:子视图们固定在 contentView 坐标系里,屏幕往上滚时,bounds 的 origin 增大,子视图相对窗口自然"向上流出"。这一步彻底规避了"大 contentSize + 小 contentView"的错位问题。
四、第二步:遍历子视图,逐个定位(Stack Layout)
第 177–243 行用一个游标变量yOffsetOfCurrentSubview从 0 开始,按subviewsInLayoutOrder的顺序把每个子视图"垫高"摆放。分三种情况:
4.1 隐藏视图:跳过但占位归零
当ignoreHiddenSubviews = YES(默认开启)时,hidden的子视图会被摆到当前游标位置,但游标不增加——它不占高度,行为与 UIStackView 一致,取消隐藏后又能恢复原高度(第 182–192 行)。
4.2 子滚动视图:三段式动态伸缩(算法精华)
这是容器"魔法"所在,对每个子滚动视图依次执行三个规则:
- 规则①:还没顶到屏幕顶部→
contentOffset.y = 0,子视图就放在yOffsetOfCurrentSubview处。此时子内容"随容器自然流走",子视图自己完全不滚(第 205–208 行); - 规则②:已经顶到顶部→ 子视图 frame 钉死在
y = 容器contentOffset.y(即顶部),同时把容器偏移 - 子视图起点偏移赋给子视图自己的contentOffset。从此"惯性"无缝移交:用户手指继续下滑,滚的就是子列表内容(第 212–215 行); - 规则③:底部截断→
frame.height = min(屏幕剩余高度, 内容剩余高度),保证子视图 frame 永不超出屏幕底部,屏幕外部分高度直接"折叠"为 0(第 220–223 行)。
这三条规则合起来,就是"吸顶 + 动态伸缩":子列表未吸顶时像普通视图,吸顶后像独立的滚动列表,滚动切换完全无感知。布局完成后,游标推进contentSize.height + contentInset(第 228 行),为下一个子视图让出整块内容高度。
4.3 普通视图:直接占位
普通 UIView 没有滚动能力,简单摆到游标处,宽度撑满容器,游标推进其 frame 高度(第 229–238 行)。
每两个子视图之间还会加上spacing间隔(最后一个不加),实现类 UIStackView 的间距控制。
五、第三步:写回 contentSize 并防抖
循环结束后,所有子视图的"总逻辑高度"已经累加在游标里,最后两步收尾(第 245–256 行):
- 写回 contentSize:取
max(总高度, 可视区高度 - contentInset)作为容器内容高度。下限保证内容不足一屏时不会"空滚",并正确计入contentInset; - 偏移量防抖:如果 contentSize 变小(比如子列表数据删除)导致
contentOffset被 UIKit 自动钳位变化,就再触发一次layoutSubviews。这步保证了"内容收缩 → 偏移修正 → 子视图重新对齐"形成完整闭环,不会停留在错位的中间态。
六、锦上添花:Swizzling 让内容变化"动起来"
有个细节值得新手留意:UITableView/UICollectionView 在 Cell 更新动画结束后才写入新的 contentSize,KVO 感知太晚,容器无法同步动画。为此项目在+initialize(OLEContainerScrollView/OLEContainerScrollView.m第 24–32 行)中做了两处方法交换:
OLEContainerScrollView/OLEContainerScrollView+Swizzling.m里重写UICollectionViewLayout的finalizeCollectionViewUpdates,在动画块内主动同步 contentSize;- 对 UITableView 的私有动画结束方法做了同样的处理,并用 0.25s 动画包装,让子列表高度变化与 Cell 插入/删除动画同步进行。
底层工具OLEReplaceMethodWithBlock(OLEContainerScrollView/OLESwizzling.m)采用"优先 addMethod"的安全交换方式,避免破坏继承链。
七、动手体验:Demo 工程
想眼见为实?打开OLEContainerScrollViewDemo.xcodeproj,Demo 里准备了多种典型场景:多个 tableView 纵向堆叠、tableView 内嵌 WebView、混合 CollectionView 等。其中OLEContainerScrollViewDemo/OLESimulatedTableView.h/.m是一个"仿真表格",专门演示 Cell 复用过程,配合容器能直观看到:子列表再长,可见区也只有屏幕那么大。
使用方式极简(见OLEContainerScrollView/OLEContainerScrollView.h注释):把OLEContainerScrollView拖进 Xcode 工程,子视图全部加到它的contentView上即可。
八、小结:三步算法一图流记忆
| 步骤 | 做什么 | 一句话口诀 |
|---|---|---|
| 第一步 | 偏移量转译到 contentView.bounds | 窗口不动,移"窗" |
| 第二步 | 游标遍历:未吸顶自然流、吸顶交惯性、底部折高度 | 顶天立地,中间伸缩 |
| 第三步 | 写回 contentSize + 偏移防抖 | 收口校准,不留残差 |
最后提醒:README.md中已注明该项目处于非维护状态,但作者自己也说"代码里有很多好想法"——这正是它作为学习 UIScrollView 高级布局的绝佳素材的原因。建议配合 Demo 逐行调试layoutSubviews,三步算法很快就能内化为你自己项目里可复用的模式。
【免费下载链接】OLEContainerScrollViewA UIScrollView subclass that intelligently handles multiple child scroll views and does not interfere with UIKitʼs cell reuse functionality.项目地址: https://gitcode.com/gh_mirrors/ol/OLEContainerScrollView
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考