UGUI源碼剖析(9):布局的實現——LayoutGroup的算法與實踐

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;中的圖像預處理…