using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; [AddComponentMenu("Layout/Flow Layout Group")] public class FlowLayoutGroup : LayoutGroup { [SerializeField] private float spacingX = 10f; [SerializeField] private float spacingY = 10f; [SerializeField] private bool expandChildHeight = false; [SerializeField] private float forcedChildHeight = 120f; private readonly List rows = new List(); private float calculatedPreferredHeight; private struct RowInfo { public int startIndex; public int endIndex; public float width; public float height; } public float SpacingX { get => spacingX; set => SetProperty(ref spacingX, value); } public float SpacingY { get => spacingY; set => SetProperty(ref spacingY, value); } public bool ExpandChildHeight { get => expandChildHeight; set => SetProperty(ref expandChildHeight, value); } public float ForcedChildHeight { get => forcedChildHeight; set => SetProperty(ref forcedChildHeight, value); } public override void CalculateLayoutInputHorizontal() { base.CalculateLayoutInputHorizontal(); CalculateRows(); float minWidth = padding.horizontal; float preferredWidth = rectTransform.rect.width > 0f ? rectTransform.rect.width : minWidth; SetLayoutInputForAxis(minWidth, preferredWidth, -1f, 0); } public override void CalculateLayoutInputVertical() { CalculateRows(); SetLayoutInputForAxis(calculatedPreferredHeight, calculatedPreferredHeight, -1f, 1); } public override void SetLayoutHorizontal() { CalculateRows(); SetChildrenAlongAxis(); } public override void SetLayoutVertical() { CalculateRows(); SetChildrenAlongAxis(); } private void CalculateRows() { rows.Clear(); float availableWidth = GetAvailableWidth(); float currentRowWidth = 0f; float currentRowHeight = 0f; int currentRowStart = 0; bool hasRow = false; for (int i = 0; i < rectChildren.Count; i++) { RectTransform child = rectChildren[i]; if (child == null) continue; float childWidth = Mathf.Max(LayoutUtility.GetPreferredSize(child, 0), LayoutUtility.GetMinSize(child, 0), child.rect.width); float childHeight = expandChildHeight ? forcedChildHeight : Mathf.Max(LayoutUtility.GetPreferredSize(child, 1), LayoutUtility.GetMinSize(child, 1), child.rect.height); float requiredWidth = hasRow ? currentRowWidth + spacingX + childWidth : childWidth; bool shouldWrap = hasRow && requiredWidth > availableWidth; if (shouldWrap) { rows.Add(new RowInfo { startIndex = currentRowStart, endIndex = i - 1, width = currentRowWidth, height = currentRowHeight }); currentRowStart = i; currentRowWidth = childWidth; currentRowHeight = childHeight; } else { currentRowWidth = hasRow ? requiredWidth : childWidth; currentRowHeight = Mathf.Max(currentRowHeight, childHeight); hasRow = true; } } if (hasRow) { rows.Add(new RowInfo { startIndex = currentRowStart, endIndex = rectChildren.Count - 1, width = currentRowWidth, height = currentRowHeight }); } calculatedPreferredHeight = padding.vertical; for (int i = 0; i < rows.Count; i++) { calculatedPreferredHeight += rows[i].height; if (i < rows.Count - 1) { calculatedPreferredHeight += spacingY; } } } private void SetChildrenAlongAxis() { float availableWidth = GetAvailableWidth(); float y = padding.top; for (int rowIndex = 0; rowIndex < rows.Count; rowIndex++) { RowInfo row = rows[rowIndex]; float startX = GetRowStartX(availableWidth, row.width); float x = startX; for (int i = row.startIndex; i <= row.endIndex; i++) { RectTransform child = rectChildren[i]; if (child == null) continue; float childWidth = Mathf.Max(LayoutUtility.GetPreferredSize(child, 0), LayoutUtility.GetMinSize(child, 0), child.rect.width); float childHeight = expandChildHeight ? forcedChildHeight : Mathf.Max(LayoutUtility.GetPreferredSize(child, 1), LayoutUtility.GetMinSize(child, 1), child.rect.height); float offsetY = GetChildVerticalOffset(row.height, childHeight); SetChildAlongAxis(child, 0, x, childWidth); SetChildAlongAxis(child, 1, y + offsetY, childHeight); x += childWidth + spacingX; } y += row.height + spacingY; } } private float GetRowStartX(float availableWidth, float rowWidth) { TextAnchor anchor = childAlignment; switch (anchor) { case TextAnchor.UpperCenter: case TextAnchor.MiddleCenter: case TextAnchor.LowerCenter: return padding.left + Mathf.Max(0f, (availableWidth - rowWidth) * 0.5f); case TextAnchor.UpperRight: case TextAnchor.MiddleRight: case TextAnchor.LowerRight: return padding.left + Mathf.Max(0f, availableWidth - rowWidth); default: return padding.left; } } private float GetChildVerticalOffset(float rowHeight, float childHeight) { TextAnchor anchor = childAlignment; switch (anchor) { case TextAnchor.MiddleLeft: case TextAnchor.MiddleCenter: case TextAnchor.MiddleRight: return Mathf.Max(0f, (rowHeight - childHeight) * 0.5f); case TextAnchor.LowerLeft: case TextAnchor.LowerCenter: case TextAnchor.LowerRight: return Mathf.Max(0f, rowHeight - childHeight); default: return 0f; } } private float GetAvailableWidth() { float availableWidth = rectTransform.rect.width - padding.horizontal; if (availableWidth > 0f) { return availableWidth; } RectTransform parentRect = rectTransform.parent as RectTransform; if (parentRect != null) { availableWidth = parentRect.rect.width - padding.horizontal; if (availableWidth > 0f) { return availableWidth; } } return float.PositiveInfinity; } }