UGUI源码剖析(第九章):布局的实现——LayoutGroup的算法与实践

在前一章中,我们剖析了LayoutRebuilder是如何调度布局重建的。现在,我们将深入到布局核心,去看看那些具体的组件——LayoutGroup系列组件是如何响应指令,并执行其各自独特的、充满数学细节的布局算法的。这将是一次深入到UGUI自动布局系统“应用层”源码的分析旅程。

1. LayoutGroup:所有布局组的“抽象基石”

LayoutGroup是一个抽象基类,它为所有具体的布局组(Horizontal, Vertical, Grid)提供了共享的基础设施和核心逻辑。

1.1 核心数据成员与属性

  • [SerializeField] protected RectOffset m_Padding:定义了布局组内容区域与其RectTransform边界之间的内边距。
  • [SerializeField] protected TextAnchor m_ChildAlignment:定义了当子元素未占满全部分配空间时,它们在容器内的对齐方式。
  • protected DrivenRectTransformTracker m_Tracker:这是至关重要的一个成员。每一个LayoutGroup都拥有一个自己的DrivenRectTransformTracker实例,用于记录和管理所有被它所控制的子RectTransform的属性。当LayoutGroup被禁用时,它会调用m_Tracker.Clear(),将被驱动的属性**“释放”**,将控制权还给用户。
  • private List m_RectChildren:一个用于缓存有效子元素的列表。这个列表在每次布局计算开始时被重新填充,是所有后续算法的操作对象。

1.2 核心方法:CalculateLayoutInputHorizontal() (第一阶段的入口)

这个方法虽然名为Horizontal,但它实际上是所有LayoutGroup第一阶段布局计算的通用入口

// LayoutGroup.cs
public virtual void CalculateLayoutInputHorizontal()
{m_RectChildren.Clear();var toIgnoreList = ListPool<Component>.Get(); // 使用对象池避免GCfor (int i = 0; i < rectTransform.childCount; i++){var rect = rectTransform.GetChild(i) as RectTransform;if (rect == null || !rect.gameObject.activeInHierarchy)continue;// 查找子对象上所有实现ILayoutIgnorer的组件rect.GetComponents(typeof(ILayoutIgnorer), toIgnoreList);if (toIgnoreList.Count == 0){m_RectChildren.Add(rect); // 如果没有忽略器,直接添加continue;}// 如果有,则遍历检查ignoreLayout属性for (int j = 0; j < toIgnoreList.Count; j++){var ignorer = (ILayoutIgnorer)toIgnoreList[j];if (!ignorer.ignoreLayout){m_RectChildren.Add(rect); // 只要有一个忽略器不要求忽略,就添加break;}}}ListPool<Component>.Release(toIgnoreList);m_Tracker.Clear(); // 在每次计算开始前,清空之前的驱动记录
}
  • 子元素筛选:这个方法的核心职责,是准备好本次布局计算所需要处理的、所有有效的子元素列表 m_RectChildren。它会遍历所有子Transform,并排除掉那些inactive的、或者被ILayoutIgnorer组件(如LayoutElement的ignoreLayout属性)标记为应忽略的子对象。
  • 驱动器重置:m_Tracker.Clear()这一行至关重要。它确保了在每次布局重建开始时,LayoutGroup都放弃了对子元素的所有旧的控制权,准备根据新的计算结果,建立新的驱动关系。

1.3 核心方法:SetDirty()

当LayoutGroup的任何属性(如padding, spacing)发生变化时,都会调用SetDirty()。

// LayoutGroup.cs
protected void SetDirty()
{if (!IsActive())return;if (!CanvasUpdateRegistry.IsRebuildingLayout())LayoutRebuilder.MarkLayoutForRebuild(rectTransform);elseStartCoroutine(DelayedSetDirty(rectTransform));
}IEnumerator DelayedSetDirty(RectTransform rectTransform)
{yield return null;LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}
  • 防止循环重建:if (!CanvasUpdateRegistry.IsRebuildingLayout())这个判断是防止无限循环重建的关键。如果当前已经处于一个布局重建的流程中,再次立即调用MarkLayoutForRebuild可能会导致循环依赖。
  • 延迟重建:为了解决上述问题,当检测到已经在重建循环中时,它会启动一个协程DelayedSetDirty,将本次重建请求,延迟到下一帧执行。这是一个非常精巧的设计,保证了布局系统的稳定性。

2. HorizontalOrVerticalLayoutGroup:线性布局的算法核心

这个抽象基类实现了线性布局(水平或垂直)最核心的计算和应用逻辑。

2.1 CalcAlongAxis:自下而上计算聚合尺寸

这是布局计算阶段的核心算法。

// HorizontalOrVerticalLayoutGroup.cs
protected void CalcAlongAxis(int axis, bool isVertical)
{// ...bool alongOtherAxis = (isVertical ^ (axis == 1));// ...for (int i = 0; i < rectChildren.Count; i++){// ... 获取子元素的min, preferred, flexible尺寸 ...if (alongOtherAxis) // 计算交叉轴{totalMin = Mathf.Max(min + combinedPadding, totalMin);// ...}else // 计算主轴{totalMin += min + spacing;// ...}}// ...SetLayoutInputForAxis(totalMin, totalPreferred, totalFlexible, axis);
}
  • alongOtherAxis的精妙判断:isVertical ^ (axis == 1)这个**异或(XOR)**运算,是一种非常高效的逻辑判断。
    • Horizontal (isVertical=false):
      • 计算水平轴(axis=0)时, false ^ false = false -> !alongOtherAxis -> 主轴逻辑。
      • 计算垂直轴(axis=1)时, false ^ true = true -> alongOtherAxis -> 交叉轴逻辑。
    • Vertical (isVertical=true):
      • 计算水平轴(axis=0)时, true ^ false = true -> alongOtherAxis -> 交叉轴逻辑。
      • 计算垂直轴(axis=1)时, true ^ true = false -> !alongOtherAxis -> 主轴逻辑。
  • 算法核心
    • 主轴尺寸:一个线性布局在主轴上所需的总尺寸,等于所有子元素的尺寸之和,加上它们之间的间距(spacing)之和
    • 交叉轴尺寸:一个线性布局在交叉轴上所需的总尺寸,取决于所有子元素中,尺寸最大的那一个

2.2 SetChildrenAlongAxis:自上而下应用位置和尺寸

这是布局应用阶段的核心算法,其逻辑非常复杂,可以分解为几步:

  1. 计算总空间和剩余空间

    float size = rectTransform.rect.size[axis]; // 获取父容器的可用空间
    float surplusSpace = size - GetTotalPreferredSize(axis); // 剩余空间 = 可用空间 - 所有子元素首选尺寸之和
    
  2. 计算弹性空间分配系数

    float itemFlexibleMultiplier = 0;
    if (surplusSpace > 0)
    {if (GetTotalFlexibleSize(axis) > 0)itemFlexibleMultiplier = surplusSpace / GetTotalFlexibleSize(axis);
    }
    

    这计算出了每一个flexible单位可以分配到多少像素的额外空间。

  3. 计算最小/首选尺寸间的插值系数

    float minMaxLerp = 0;
    if (GetTotalMinSize(axis) != GetTotalPreferredSize(axis))minMaxLerp = Mathf.Clamp01((size - GetTotalMinSize(axis)) / (GetTotalPreferredSize(axis) - GetTotalMinSize(axis)));
    

    如果父容器的可用空间size不足以满足所有子元素的preferredSize,但又大于minSize,这个minMaxLerp系数就决定了子元素最终尺寸在min和preferred之间的“压缩”程度。

  4. 遍历并设置子元素

    for (...)
    {// ...// 核心公式:计算子元素最终尺寸float childSize = Mathf.Lerp(min, preferred, minMaxLerp);childSize += flexible * itemFlexibleMultiplier;if (controlSize){// 如果LayoutGroup控制尺寸,则应用计算出的childSizeSetChildAlongAxisWithScale(child, axis, pos, childSize, scaleFactor);}else{// 如果不控制尺寸,则只设置位置,并考虑对齐float offsetInCell = (childSize - child.sizeDelta[axis]) * alignmentOnAxis;SetChildAlongAxisWithScale(child, axis, pos + offsetInCell, scaleFactor);}pos += childSize * scaleFactor + spacing; // 更新下一个元素的起始位置
    }
    

最终尺寸公式:childSize的计算是整个算法的精华。它首先在min和preferred之间进行插值(处理空间不足的情况),然后再叠加上根据flexible权重分配到的额外空间(处理空间富余的情况)。

SetChildAlongAxisWithScale: 这个辅助方法,最终会调用m_Tracker.Add(…)来记录LayoutGroup正在驱动子元素的哪些RectTransform属性,并将计算出的pos和size应用到子元素的anchoredPosition和sizeDelta上。

3. GridLayoutGroup:二维网格的布局算法

GridLayoutGroup的算法更为独立,它不使用HorizontalOrVerticalLayoutGroup的基类方法。

3.1 尺寸计算阶段 (CalculateLayoutInput…)
其核心是根据约束(Constraint)模式,来推算出网格的行列数,进而计算出整个组的总尺寸。

  • FixedColumnCount: 列数固定,总宽度固定。总高度则取决于总行数(总元素数 / 列数)。
  • Flexible: 在灵活模式下,它会尝试根据当前父容器的可用宽度,来计算出每行能放下的单元格数量,再由此推算出总行数,并以此来计算总的首选高度。

3.2 布局应用阶段 (SetLayoutVertical)
GridLayoutGroup巧妙地将所有位置和尺寸的设置,都放在了SetLayoutVertical这一个阶段。

// GridLayoutGroup.cs
public override void SetLayoutHorizontal()
{// 在水平布局阶段,只设置所有子元素的尺寸为固定的cellSizefor (int i = 0; i < rectChildrenCount; i++){// ...rect.sizeDelta = cellSize;}
}public override void SetLayoutVertical()
{// 在垂直布局阶段,此时所有子元素的尺寸都已确定// 1. 根据约束和可用空间,计算出最终的行列数 (cellCountX, cellCountY)// ...// 2. 循环遍历所有子元素for (int i = 0; i < rectChildrenCount; i++){// 3. 根据startAxis和索引i,通过取模(%)和整除(/)运算,计算出该元素的二维网格坐标(positionX, positionY)// ...// 4. 根据网格坐标、cellSize和spacing,计算出最终的本地位置// ...// 5. 调用SetChildAlongAxis,将位置和尺寸应用到子元素SetChildAlongAxis(rectChildren[i], 0, ...);SetChildAlongAxis(rectChildren[i], 1, ...);}
}

这种“先在Horizontal阶段统一尺寸,再在Vertical阶段统一位置”的策略,完美地契合了UGUI的两遍式布局管线。它确保了在计算最终位置时,所有子元素的尺寸都已经是一个已知的、固定的值,从而大大简化了布局算法的复杂性。

总结:

通过对LayoutGroup系列组件的源码级分析,我们得以一窥UGUI自动布局系统强大功能背后的算法实现。

  • Horizontal/VerticalLayoutGroup 的核心,是一套基于主轴/交叉轴概念的、分别进行累加取最大值的聚合算法,并通过一个精密的插值与弹性分配公式,来最终确定每一个子元素的位置和尺寸。
  • GridLayoutGroup 则通过约束模式,预先计算出网格的维度,然后在水平布局阶段统一设置尺寸,在垂直布局阶段再根据行列坐标,统一设置位置。

理解了这些组件在CalculateLayoutInput和SetLayout这两个核心阶段的不同算法和行为,不仅能帮助我们更精确地使用它们,更能让我们在面对布局相关的性能问题时,清晰地知道其背后高昂的遍历、查询和计算代价究竟从何而来,从而做出更明智的优化决策。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/919202.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/919202.shtml
英文地址,请注明出处:http://en.pswp.cn/news/919202.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GitHub PR 提交流程

step1 在 GitHub 上 fork 目标仓库&#xff08;手动操作&#xff09; step2 将 fork 的目标仓库克隆到本地 git clone https://github.com/<your-username>/<repo-name>.git cd <repo-name>step3 与上游目标仓库建立链接 git remote add upstream https://gi…

矿物分类案列 (一)六种方法对数据的填充

目录 矿物数据项目介绍&#xff1a; 数据问题与处理方案&#xff1a; 数据填充策略讨论&#xff1a; 模型选择与任务类型&#xff1a; 模型训练计划&#xff1a; 一.数据集填充 1.读取数据 2.把标签转化为数值 3.把异常数据转化为nan 4.数据Z标准化 5.划分训练集测试…

vue:vue3的方法torefs和方法toref

在 Vue 3 的 Composition API 中,toRef 和 toRefs 是两个用于处理响应式数据的重要工具,它们专门用于从 reactive() 对象中提取属性并保持响应性。 toRef() 作用:将 reactive 对象的单个属性转换为一个 ref 对象,保持与源属性的响应式连接。 使用场景: 需要单独提取 rea…

Android 移动端 UI 设计:前端常用设计原则总结

在 Android 移动端开发中&#xff0c;优秀的 UI 设计不仅需要视觉上的美观&#xff0c;更需要符合用户习惯、提升操作效率的设计逻辑。前端 UI 设计原则是指导开发者将功能需求转化为优质用户体验的核心准则&#xff0c;这些原则贯穿于布局结构、交互反馈、视觉呈现等各个环节。…

计算机网络 TCP三次握手、四次挥手超详细流程【报文交换、状态变化】

TCP&#xff08;传输控制协议&#xff09;是互联网最重要的协议之一&#xff0c;它保证了数据的可靠、有序传输。连接建立时的“三次握手”和连接关闭时的“四次挥手”是其核心机制&#xff0c;涉及特定的报文交换和状态变化。 一、TCP 三次握手&#xff08;Three-Way Handshak…

使用Applications Manager进行 Apache Solr 监控

Apache Solr 为一些对性能极为敏感的环境提供搜索支持&#xff1a;电子商务、企业应用、内容门户和内部知识系统。因此&#xff0c;当出现延迟增加或结果不一致的情况时&#xff0c;用户会立刻察觉。而当这些问题未被发现时&#xff0c;情况会迅速恶化。 Apache Solr 基于 Apa…

Shell脚本-for循环语法结构

一、前言在 Linux Shell 脚本编程中&#xff0c;for 循环 是最常用的控制结构之一&#xff0c;用于重复执行一段命令&#xff0c;特别适用于处理列表、文件、数字序列等场景。本文将详细介绍 Shell 脚本中 for 循环的各种语法结构&#xff0c;包括&#xff1a;✅ 经典 for in 结…

记SpringBoot3.x + Thymeleaf 项目实现(MVC架构模式)

目录 前言 一、创建SpringBoot项目 1. 创建项目 2. 运行项目 二、连接数据库实现登录 1. pom.xml文件引入依赖包 2. application.yml文件配置 3. 数据持久层&#xff0c;mybatis操作映射 4. Service接口及实现 5. Controller代码 6. Thymeleaf页面登录 7. 运行项目…

Java 导出word 实现表格内插入图表(柱状图、折线图、饼状图)--可编辑数据

表格内插入图表导出效果表格内图表生成流程分析 核心问题与解决方案 问题 Word 图表作为独立对象&#xff0c;容易与文本分离位置难以精确控制&#xff0c;编辑时容易偏移缺乏与表格数据的关联性 解决方案 直接嵌入&#xff1a;将图表嵌入表格单元格&#xff0c;确保数据关联精…

北京JAVA基础面试30天打卡12

1.MySQL中count(*)、count(I)和count(字段名)有什么区别&#xff1f; 1**.COUNT ()**是效率最高的统计方式&#xff1a;COUNT()被优化为常量&#xff0c;直接统计表的所有记录数&#xff0c;不依赖字段内容&#xff0c;开销最低。推荐在统计整个表的记录数时使用。 2.**COUNT(1…

【AI】——结合Ollama、Open WebUI和Docker本地部署可视化AI大语言模型

&#x1f3bc;个人主页&#xff1a;【Y小夜】 &#x1f60e;作者简介&#xff1a;一位双非学校的大三学生&#xff0c;编程爱好者&#xff0c; 专注于基础和实战分享&#xff0c;欢迎私信咨询&#xff01; &#x1f386;入门专栏&#xff1a;&#x1f387;【MySQL&#xff0…

RAG学习(二)

构建索引 一、向量嵌入 向量嵌入&#xff08;Embedding&#xff09;是一种将真实世界中复杂、高维的数据对象&#xff08;如文本、图像、音频、视频等&#xff09;转换为数学上易于处理的、低维、稠密的连续数值向量的技术。 想象一下&#xff0c;我们将每一个词、每一段话、…

亚马逊店铺绩效巡检_影刀RPA源码解读

一、项目简介 本项目是一个基于RPA开发的店铺绩效巡店机器人。该机器人能够自动化地登录卖家后台&#xff0c;遍历多个店铺和站点&#xff0c;收集并分析各类绩效数据&#xff0c;包括政策合规性、客户服务绩效、配送绩效等关键指标&#xff0c;并将数据整理到Excel报告中&…

跨越南北的养老对话:为培养“银发中国”人才注入新动能

2025年8月16日&#xff0c;北京养老行业协会常务副会长陈楫宝一行到访广州市白云区粤荣职业培训学校&#xff0c;受到颐年集团副总李娜的热情接待。此次访问不仅是京穗两地养老行业的一次深度交流&#xff0c;更为推动全国智慧养老体系建设、提升养老服务专业化水平注入了新动能…

Spring IOC 学习笔记

1. 概述Spring IOC&#xff08;Inversion of Control&#xff0c;控制反转&#xff09;是一种设计思想&#xff0c;通过依赖注入&#xff08;Dependency Injection&#xff0c;DI&#xff09;实现。它的核心思想是将对象的创建和依赖关系的管理交给Spring容器&#xff0c;从而降…

揭开Android Vulkan渲染封印:帧率暴增的底层指令

ps&#xff1a;本文内容较干&#xff0c;建议收藏后反复边跟进源码边思考设计思想。壹渲染管线的基础架构为什么叫渲染管线&#xff1f;这里是因为整个渲染的过程涉及多道工序&#xff0c;像管道里的流水线一样&#xff0c;一道一道的处理数据的过程&#xff0c;所以使用渲染管…

HTTP 请求转发与重定向详解及其应用(含 Java 示例)

在 Web 开发中&#xff0c;我们经常需要在不同页面之间跳转&#xff0c;比如登录成功后跳到首页、提交表单后跳到结果页面。这时&#xff0c;常见的两种跳转方式就是 请求转发&#xff08;Request Forward&#xff09; 和 重定向&#xff08;Redirect&#xff09;。虽然它们都能…

如何将 MCP Server (FastMCP) 配置为公网访问(监听 0.0.0.0)

如何将 MCP Server &#xff08;FastMCP&#xff09; 配置为公网访问&#xff08;监听 0.0.0.0&#xff09;引言常见错误尝试根本原因&#xff1a;从源码解析正确的解决方案总结引言 在使用 Model Context Protocol(MCP) 框架开发自定义工具服务器时&#xff0c;我们经常使用 …

The Network Link Layer: 无线传感器中Delay Tolerant Networks – DTNs 延迟容忍网络

Delay Tolerant Networks – DTNs 延迟容忍网络架构归属Delay Tolerant Networks – DTNs 延迟容忍网络应用实例例子 1&#xff1a;瑞典北部的萨米人 (Saami reindeer herders)例子 2&#xff1a;太平洋中的动物传感网络DTNs路由方式——存储&转发DTNs移动模型Random walk …

计算机视觉(opencv)实战二——图像边界扩展cv2.copyMakeBorder()

OpenCV copyMakeBorder() 图像边界扩展详解与实战在图像处理和计算机视觉中&#xff0c;有时需要在原始图像的四周增加边界&#xff08;Padding&#xff09;。这种操作在很多场景中都有应用&#xff0c;比如&#xff1a;卷积神经网络&#xff08;CNN&#xff09;中的图像预处理…