news 2026/8/22 14:15:02

OLEContainerScrollView源码解析(二):layoutSubviews三步布局算法深度拆解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OLEContainerScrollView源码解析(二):layoutSubviews三步布局算法深度拆解

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
  • 普通视图 → 监听其framebounds

任何一项变化都会调用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 行)。巧妙之处在于:

  1. contentView.frame = self.bounds:contentView 永远和可视区等大、等大居中,它本身不需要做成整个内容那么高
  2. 把容器的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 行):

  1. 写回 contentSize:取max(总高度, 可视区高度 - contentInset)作为容器内容高度。下限保证内容不足一屏时不会"空滚",并正确计入contentInset
  2. 偏移量防抖:如果 contentSize 变小(比如子列表数据删除)导致contentOffset被 UIKit 自动钳位变化,就再触发一次layoutSubviews。这步保证了"内容收缩 → 偏移修正 → 子视图重新对齐"形成完整闭环,不会停留在错位的中间态。

六、锦上添花:Swizzling 让内容变化"动起来"

有个细节值得新手留意:UITableView/UICollectionView 在 Cell 更新动画结束后才写入新的 contentSize,KVO 感知太晚,容器无法同步动画。为此项目在+initializeOLEContainerScrollView/OLEContainerScrollView.m第 24–32 行)中做了两处方法交换:

  • OLEContainerScrollView/OLEContainerScrollView+Swizzling.m里重写UICollectionViewLayoutfinalizeCollectionViewUpdates,在动画块内主动同步 contentSize;
  • 对 UITableView 的私有动画结束方法做了同样的处理,并用 0.25s 动画包装,让子列表高度变化与 Cell 插入/删除动画同步进行

底层工具OLEReplaceMethodWithBlockOLEContainerScrollView/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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/22 14:13:34

LLaMA-Factory MoE模型微调实战:3 步跑通 30B-A3B,不再爆显存

LLaMA-Factory MoE模型微调实战:3 步跑通 30B-A3B,不再爆显存 【免费下载链接】LlamaFactory Unified Efficient Fine-Tuning of 100 LLMs & VLMs (ACL 2024) 项目地址: https://gitcode.com/GitHub_Trending/ll/LlamaFactory 做 LLaMA-Facto…

作者头像 李华
网站建设 2026/8/22 14:12:37

Dark Reader 暗黑模式教程:快速让网页夜间阅读不刺眼

Dark Reader 暗黑模式教程:快速让网页夜间阅读不刺眼 【免费下载链接】darkreader Dark Reader Chrome and Firefox extension 项目地址: https://gitcode.com/gh_mirrors/da/darkreader 如果你经常在深夜浏览网页,刺眼的白色背景确实很伤眼睛。D…

作者头像 李华
网站建设 2026/8/22 14:12:15

Tetrigo如何用SQLite记录你的高光时刻:本地排行榜从零到完整实现

Tetrigo如何用SQLite记录你的高光时刻:本地排行榜从零到完整实现 【免费下载链接】tetrigo Play Tetris in your terminal. 项目地址: https://gitcode.com/gh_mirrors/te/tetrigo Tetrigo 是一款用 Go 语言编写的终端俄罗斯方块游戏,它最大的亮点…

作者头像 李华