ARTICLE · 1094754
【Unity UGUI源码深度解析】17|ContentSizeFitter与AspectRatioFitter解析:自适应尺寸与布局驱动冲突
《UGUI源码深度解析》第 17 篇 · 界面小组工作日志基准:Unity 2022.3.62f2c1 / 本地 UGUI 1.0.0。人物与项目情节为虚构;源码机制以本地实现为准。

一、尺寸刚改好,怎么又弹回去了?
阿澈拖动详情图片的宽度,松手后数值又变了。它的父对象有 HorizontalLayoutGroup,自己有 ContentSizeFitter,旁边还准备加 AspectRatioFitter。
我第一反应是“布局有延迟”,阿澈却指着 Inspector:“也可能是有三个人都在改这一个数。”
这次他的猜测更接近根因。我们不先加 ForceRebuild,而是列一张控制权表:谁读宽度,谁写宽度,谁又根据宽度写高度。
二、ContentSizeFitter只负责改自己
ContentSizeFitter 是 ILayoutSelfController。横纵轴分别选择 Unconstrained、MinSize 或 PreferredSize。它通过 LayoutUtility 读取本对象上的布局需求,再调用 SetSizeWithCurrentAnchors 调整自身尺寸。
源码的核心选择如下:
if(fitting == FitMode.MinSize) rectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetMinSize(m_Rect, axis));else rectTransform.SetSizeWithCurrentAnchors((RectTransform.Axis)axis, LayoutUtility.GetPreferredSize(m_Rect, axis));它不会自己逐个遍历所有孩子并计算文本大小。如果同一对象上有布局组,布局组先汇总孩子需求,Fitter 再读取这份报告。只有 Fitter、没有有效需求提供者时,不应期待它猜出内容范围。
Unconstrained 则不控制对应轴。这是分配控制权的重要选项,不是功能缺一半。例如宽由父视口控制、高由内容决定,就是很常见的合理分工。
三、AspectRatioFitter不问内容想多大
AspectRatioFitter 根据比例关系修改矩形。WidthControlsHeight 使用 height=width/aspectRatio,HeightControlsWidth 使用 width=height×aspectRatio。
FitInParent 让自身完整容纳在父区域中,可能留下空白;EnvelopeParent 覆盖父区域,可能超出某一方向。两者还会调整锚点和 anchoredPosition,不只是写一个孤立的 sizeDelta。
“那它会遵守 Text 的 preferredHeight 吗?”
不会自动把文本需求与比例做约束求解。它追求的是几何比例,ContentSizeFitter 追求的是布局报告的尺寸,目标不同。两者若同时写同一轴,可能本来就没有一致答案。
当前实现还有对象有效性和父对象存在等检查。放在特殊 Canvas 根节点或不满足约束的对象上时,不能直接套用普通子矩形的结论。

这张图表达的是一种职责拆分示意,不是要求把两条链强行挂到同一个对象上。
四、DrivenRectTransformTracker不是抢锁工具
两个适配器都使用 DrivenRectTransformTracker,登记它们驱动的属性,禁用或重新配置时清理记录。这有助于编辑器显示受驱动状态,并管理相关驱动关系。
但它不是互斥锁,也不会自动阻止另一个组件写相同属性。看到某个字段变灰,应该去找控制器,而不是推断系统已经帮我们选出了唯一赢家。
父布局写孩子宽度,孩子 Fitter 又读自身 preferredWidth 改宽;尺寸变化触发回调,再标记布局。即使没有无限循环,也可能发生重复重建或在某个时序下被覆盖。
阿澈说:“所以问题不一定表现为一直抖,也可能只是我设置的值总留不住。”
正是。没有报错并不等于控制权设计清晰。
五、拆一层容器,往往比多刷新一次有效
我们给图片外面加一个由父布局控制的槽位,内部图片用锚点和比例组件表现;父布局只认识槽位,不再直接控制内部图片的同一尺寸。
文字区域则固定宽度,由 Text 报告换行后的高度,外层布局组汇总,ContentSizeFitter 只拟合内容容器的纵轴。这样宽到高的依赖是单向的。
这不是说 LayoutGroup 与 ContentSizeFitter 永远不能同挂。常见内容容器恰恰会把两者结合:布局组报告并排列孩子,Fitter 修改容器自身。真正要避免的是同一个属性被不一致的规则同时控制,尤其来自父组与自身适配器的竞争。
“多一层对象会不会更慢?”
层级也有成本,但不能脱离规模判断。先让依赖正确,再测是否值得合并。用频繁强制重建掩盖结构冲突,通常比一层职责明确的容器更难维护。
六、按轴建立一份验收记录
最小反例是父 HorizontalLayoutGroup 控制子宽,子 ContentSizeFitter 也拟合横轴。改变内容,在 SetLayoutHorizontal 和尺寸变化回调处观察谁先后写入。
修复组关闭子横向拟合,或拆出内部显示节点,再比较重建次数和最终宽度。比例组固定父区域,分别测试 FitInParent 与 EnvelopeParent,确认一个保完整、一个保覆盖。
同时记录锚点、pivot、父布局控制项和 Fitter 模式。只记“宽度 200”不足以重现问题。本文没有进行 Unity 场景实测,给出的是源码驱动的检查步骤。
布局这一轮到这里收束。阿澈说背包终于站整齐了,可有个透明面板还在挡按钮。下一篇,我们回到 EventSystem,追踪输入到底交给了谁。
本篇源码索引
ContentSizeFitter: ./com.unity.ugui@1.0.0/Runtime/UI/Core/Layout/ContentSizeFitter.csAspectRatioFitter: ./com.unity.ugui@1.0.0/Runtime/UI/Core/Layout/AspectRatioFitter.csLayoutRebuilder: ./com.unity.ugui@1.0.0/Runtime/UI/Core/Layout/LayoutRebuilder.cs