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/OLEContainerScrollViewOLEContainerScrollView 是一个经典的 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。任何一项变化都会调用setNeedsLayoutlayoutIfNeeded立即重算布局。这就是子列表数据一变容器自动调整总高度的原理全程无轮询、无定时器。子视图的排列顺序由私有数组subviewsInLayoutOrder维护它在子视图被加入contentView时同步更新didAddSubviewToContainer:第 78–108 行。这个中介层正是OLEContainerScrollView/OLEContainerScrollViewContentView.m的职责它重写了didAddSubview:/willRemoveSubview:把增删事件转发给容器。三、第一步偏移量转译Offset Translationself.contentView.frame self.bounds; self.contentView.bounds (CGRect){ self.contentOffset, self.contentView.bounds.size };这两行是整个算法的地基第 168–169 行。巧妙之处在于contentView.frame self.boundscontentView 永远和可视区等大、等大居中它本身不需要做成整个内容那么高把容器的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 更新动画结束后才写入新的 contentSizeKVO 感知太晚容器无法同步动画。为此项目在initializeOLEContainerScrollView/OLEContainerScrollView.m第 24–32 行中做了两处方法交换OLEContainerScrollView/OLEContainerScrollViewSwizzling.m里重写UICollectionViewLayout的finalizeCollectionViewUpdates在动画块内主动同步 contentSize对 UITableView 的私有动画结束方法做了同样的处理并用 0.25s 动画包装让子列表高度变化与 Cell 插入/删除动画同步进行。底层工具OLEReplaceMethodWithBlockOLEContainerScrollView/OLESwizzling.m采用优先 addMethod的安全交换方式避免破坏继承链。七、动手体验Demo 工程想眼见为实打开OLEContainerScrollViewDemo.xcodeprojDemo 里准备了多种典型场景多个 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),仅供参考