using UnityEngine; using System.Collections.Generic; namespace Flexalon { /// /// Use a flexible layout to position children linearly along the x, y, or z axis. /// The sizes of the children are considered so that they are evenly spaced. /// [AddComponentMenu("Flexalon/Flexalon Flexible Layout"), HelpURL("https://www.flexalon.com/docs/flexibleLayout")] public class FlexalonFlexibleLayout : LayoutBase { /// Determines how the space between children is distributed. public enum GapOptions { /// The Gap/WrapGap property determines the space between children. Fixed, /// Space is added between children to fill the available space. SpaceBetween } [SerializeField] private Direction _direction = Direction.PositiveX; /// The direction in which objects are placed, one after the other. public Direction Direction { get { return _direction; } set { _direction = value; _node.MarkDirty(); } } [SerializeField] private bool _wrap; /// If set, then the flexible layout will attempt to position children in a line /// along the Direction axis until it runs out of space. Then it will start the next line by /// following the wrap direction. Wrapping will only occur if the size of the Direction axis is /// set to any value other than "Layout". public bool Wrap { get { return _wrap; } set { _wrap = value; _node.MarkDirty(); } } [SerializeField] private Direction _wrapDirection = Direction.NegativeY; /// The direction to start a new line when wrapping. public Direction WrapDirection { get { return _wrapDirection; } set { _wrapDirection = value; _node.MarkDirty(); } } [SerializeField] private Align _horizontalAlign = Align.Center; /// Determines how the entire layout horizontally aligns to the parent's box. public Align HorizontalAlign { get { return _horizontalAlign; } set { _horizontalAlign = value; _node.MarkDirty(); } } [SerializeField] private Align _verticalAlign = Align.Center; /// /// Determines how the entire layout vertically aligns to the parent's box. public Align VerticalAlign { get { return _verticalAlign; } set { _verticalAlign = value; _node.MarkDirty(); } } [SerializeField] private Align _depthAlign = Align.Center; /// Determines how the entire layout aligns to the parent's box in depth. public Align DepthAlign { get { return _depthAlign; } set { _depthAlign = value; _node.MarkDirty(); } } [SerializeField] private Align _horizontalInnerAlign = Align.Center; /// The inner align property along the Direction axis will change how wrapped lines align /// with each other. The inner align property along the other two axes will change how each object lines /// up with all other objects. public Align HorizontalInnerAlign { get { return _horizontalInnerAlign; } set { _horizontalInnerAlign = value; _node.MarkDirty(); } } [SerializeField] private Align _verticalInnerAlign = Align.Center; /// The inner align property along the Direction axis will change how wrapped lines align /// with each other. The inner align property along the other two axes will change how each object lines /// up with all other objects. public Align VerticalInnerAlign { get { return _verticalInnerAlign; } set { _verticalInnerAlign = value; _node.MarkDirty(); } } [SerializeField] private Align _depthInnerAlign = Align.Center; /// The inner align property along the Direction axis will change how wrapped lines align /// with each other. The inner align property along the other two axes will change how each object lines /// up with all other objects. public Align DepthInnerAlign { get { return _depthInnerAlign; } set { _depthInnerAlign = value; _node.MarkDirty(); } } [SerializeField] private GapOptions _gapType = GapOptions.Fixed; /// Determines how the space between children is distributed. public GapOptions GapType { get { return _gapType; } set { _gapType = value; _node.MarkDirty(); } } [SerializeField] private float _gap; /// Adds a gap between objects on the Direction axis. public float Gap { get { return _gap; } set { _gap = value; _gapType = GapOptions.Fixed; _node.MarkDirty(); } } [SerializeField] private GapOptions _wrapGapType = GapOptions.Fixed; /// Determines how the space between lines is distributed. public GapOptions WrapGapType { get { return _wrapGapType; } set { _wrapGapType = value; _node.MarkDirty(); } } [SerializeField] private float _wrapGap; /// Adds a gap between objects on the Wrap Direction axis. public float WrapGap { get { return _wrapGap; } set { _wrapGap = value; _wrapGapType = GapOptions.Fixed; _node.MarkDirty(); } } private class Line { public Vector3 Size = Vector3.zero; public Vector3 Position = Vector3.zero; public List Children = new List(); public List ChildSizes = new List(); public List ChildPositions = new List(); } private List _lines = new List(); private List _flexItems = new List(); private void CreateLines(FlexalonNode node, int flexAxis, int wrapAxis, int thirdAxis, bool wrap, Vector3 size, float maxLineSize, bool measure) { _lines.Clear(); if (node.Children.Count == 0) { return; } // Divide children into lines considering: size, child sizes. var line = new Line(); _lines.Add(line); bool addGap = false; int i = 0; foreach (var child in node.Children) { var gap = (addGap && _gapType == GapOptions.Fixed ? _gap : 0); var childSize = measure ? child.GetMeasureSize(size) : child.GetArrangeSize(); if (line.ChildSizes.Count > 0 && wrap && line.Size[flexAxis] + childSize[flexAxis] + gap > maxLineSize) { line = new Line(); _lines.Add(line); addGap = false; gap = 0; i++; } FlexalonLog.Log("FlexMeasure | Add child to line", child, i); FlexalonLog.Log("FlexMeasure | Child Size", child, childSize); line.ChildSizes.Add(childSize); line.Size[flexAxis] += childSize[flexAxis] + gap; line.Size[wrapAxis] = Mathf.Max(line.Size[wrapAxis], childSize[wrapAxis]); line.Size[thirdAxis] = Mathf.Max(line.Size[thirdAxis], childSize[thirdAxis]); line.Children.Add(child); addGap = true; } } private Vector3 MeasureTotalLineSize(bool wrap, int flexAxis, int wrapAxis, int thirdAxis) { Vector3 layoutSize = Vector3.zero; foreach (var line in _lines) { if (wrap) { layoutSize[flexAxis] = Mathf.Max(layoutSize[flexAxis], line.Size[flexAxis]); layoutSize[wrapAxis] += line.Size[wrapAxis]; layoutSize[thirdAxis] = Mathf.Max(layoutSize[thirdAxis], line.Size[thirdAxis]); } else { for (int axis = 0; axis < 3; axis++) { layoutSize[axis] = Mathf.Max(layoutSize[axis], line.Size[axis]); } } } if (wrap && _wrapGapType == GapOptions.Fixed) { layoutSize[wrapAxis] += _wrapGap * (_lines.Count - 1); } return layoutSize; } private Vector3 MeasureLayoutSize(FlexalonNode node, bool wrap, int flexAxis, int wrapAxis, int thirdAxis, Vector3 size, Vector3 min, Vector3 max) { var layoutSize = MeasureTotalLineSize(wrap, flexAxis, wrapAxis, thirdAxis); for (int axis = 0; axis < 3; axis++) { if (node.GetSizeType((Axis)axis) == SizeType.Layout) { layoutSize[axis] = Mathf.Clamp(layoutSize[axis], min[axis], max[axis]); } else { layoutSize[axis] = size[axis]; } } return layoutSize; } private void SetChildSize(Line line, int index, int axis, float size, float layoutSize) { var childSize = line.ChildSizes[index]; line.Children[index].SetShrinkFillSize(axis, size, layoutSize, true); childSize[axis] = size; line.ChildSizes[index] = childSize; } private void FillFlexAxis(float size, int flexAxis) { var gap = _gapType == GapOptions.Fixed ? _gap : 0; foreach (var line in _lines) { var remainingSpace = size - line.Size[flexAxis]; if (Mathf.Abs(remainingSpace) <= 1e-6f) { continue; } _flexItems.Clear(); for (int i = 0; i < line.Children.Count; i++) { _flexItems.Add(Flex.CreateFlexItem( line.Children[i], flexAxis, line.ChildSizes[i][flexAxis], line.Size[flexAxis], size)); } Flex.GrowOrShrink(_flexItems, line.Size[flexAxis], size, gap); for (int i = 0; i < line.Children.Count; i++) { SetChildSize(line, i, flexAxis, _flexItems[i].FinalSize, size); } } } private void FillWrapAxis(float size, int wrapAxis) { _flexItems.Clear(); float remainingSpace = size; var gap = _wrapGapType == GapOptions.Fixed ? _wrapGap : 0; foreach (var line in _lines) { var item = new FlexItem(); item.StartSize = line.Size[wrapAxis]; item.MaxSize = line.Children[0].GetMaxSize(wrapAxis, size); item.ShrinkFactor = line.Size[wrapAxis] / size; item.FinalSize = line.Size[wrapAxis]; for (int i = 0; i < line.Children.Count; i++) { var child = line.Children[i]; if (child.CanShrink(wrapAxis)) { item.MinSize = Mathf.Max(child.GetMinSize(wrapAxis, size), item.MinSize); } else { item.MinSize = Mathf.Max(line.ChildSizes[i][wrapAxis], item.MinSize); } item.MaxSize = Mathf.Max(child.GetMaxSize(wrapAxis, size), item.MaxSize); if (child.GetSizeType(wrapAxis) == SizeType.Fill) { item.GrowFactor = Mathf.Max(child.SizeOfParent[wrapAxis], item.GrowFactor); } } remainingSpace -= line.Size[wrapAxis]; _flexItems.Add(item); } if (Mathf.Abs(remainingSpace) > 1e-6) { Flex.GrowOrShrink(_flexItems, size - remainingSpace, size, gap); } for (int l = 0; l < _lines.Count; l++) { var line = _lines[l]; var item = _flexItems[l]; for (int i = 0; i < line.Children.Count; i++) { var child = line.Children[i]; if (child.GetSizeType(wrapAxis) == SizeType.Fill) { var newSize = child.SizeOfParent[wrapAxis] * size; var minSize = child.GetMinSize(wrapAxis, size); var maxSize = child.GetMaxSize(wrapAxis, size); maxSize = Mathf.Min(maxSize, item.FinalSize); newSize = Mathf.Clamp(newSize, minSize, maxSize); SetChildSize(line, i, wrapAxis, newSize, size); } else { SetChildSize(line, i, wrapAxis, item.FinalSize, size); } } } } private void FillThirdAxis(float size, int thirdAxis) { foreach (var child in _node.Children) { child.SetShrinkFillSize(thirdAxis, size, size); } } private void UpdateFillSizes(Vector3 size, int flexAxis, int wrapAxis, int thirdAxis) { FillFlexAxis(size[flexAxis], flexAxis); FillWrapAxis(size[wrapAxis], wrapAxis); FillThirdAxis(size[thirdAxis], thirdAxis); } /// public override Bounds Measure(FlexalonNode node, Vector3 size, Vector3 min, Vector3 max) { FlexalonLog.Log("FlexMeasure | Size", node, size); // Gather useful data var flexAxis = (int) Math.GetAxisFromDirection(_direction); var otherAxes = Math.GetOtherAxes(flexAxis); bool childrenSizeFlexAxis = node.GetSizeType(flexAxis) == SizeType.Layout; var wrapAxis = (int) Math.GetAxisFromDirection(_wrapDirection); if (wrapAxis == flexAxis) { wrapAxis = otherAxes.Item1; } var thirdAxis = (wrapAxis == otherAxes.Item1 ? otherAxes.Item2 : otherAxes.Item1); bool wrap = (flexAxis != wrapAxis) && _wrap; var maxLineSize = childrenSizeFlexAxis ? max[flexAxis] : size[flexAxis]; FlexalonLog.Log("FlexMeasure | Flex Axis", node, flexAxis); FlexalonLog.Log("FlexMeasure | Wrap Axis", node, wrapAxis); FlexalonLog.Log("FlexMeasure | Third Axis", node, thirdAxis); FlexalonLog.Log("FlexMeasure | Wrap", node, wrap); CreateLines(node, flexAxis, wrapAxis, thirdAxis, wrap, size, maxLineSize, true); for (int i = 0; i < _lines.Count; i++) { FlexalonLog.Log("FlexMeasure | Line size " + i + " " + _lines[i].Size); } Vector3 layoutSize = MeasureLayoutSize(node, wrap, flexAxis, wrapAxis, thirdAxis, size, min, max); FlexalonLog.Log("FlexMeasure | Total Layout Size", node, layoutSize); UpdateFillSizes(layoutSize, flexAxis, wrapAxis, thirdAxis); return new Bounds(Vector3.zero, layoutSize); } /// public override void Arrange(FlexalonNode node, Vector3 layoutSize) { FlexalonLog.Log("FlexArrange | LayoutSize", node, layoutSize); // Gather useful data var flexAxis = (int) Math.GetAxisFromDirection(_direction); bool childrenSizeFlexAxis = node.GetSizeType(flexAxis) == SizeType.Layout; var otherAxes = Math.GetOtherAxes(flexAxis); var wrapAxis = (int) Math.GetAxisFromDirection(_wrapDirection); if (wrapAxis == flexAxis) { wrapAxis = otherAxes.Item1; } var thirdAxis = (wrapAxis == otherAxes.Item1 ? otherAxes.Item2 : otherAxes.Item1); bool wrap = (flexAxis != wrapAxis) && _wrap; var flexDirection = Math.GetPositiveFromDirection(_direction); var wrapDirection = Math.GetPositiveFromDirection(_wrapDirection); var align = new Align[] { _horizontalAlign, _verticalAlign, _depthAlign }; var innerAlign = new Align[] { _horizontalInnerAlign, _verticalInnerAlign, _depthInnerAlign }; FlexalonLog.Log("FlexArrange | Flex Direction", node, _direction); FlexalonLog.Log("FlexArrange | Wrap Direction", node, _wrapDirection); FlexalonLog.Log("FlexArrange | Third Axis", node, thirdAxis); FlexalonLog.Log("FlexArrange | Wrap", node, wrap); CreateLines(node, flexAxis, wrapAxis, thirdAxis, wrap, layoutSize, layoutSize[flexAxis], false); // Position children within _lines. Consider: line size, child size, flexInnerAlign { foreach (var line in _lines) { float lineGap = 0; if (line.Children.Count > 1) { switch (_gapType) { case GapOptions.Fixed: lineGap = _gap; break; case GapOptions.SpaceBetween: lineGap = (layoutSize[flexAxis] - line.Size[flexAxis]) / (line.Children.Count - 1); line.Size[flexAxis] = layoutSize[flexAxis]; break; } } float nextChildPosition = flexDirection * -line.Size[flexAxis] / 2; foreach (var childSize in line.ChildSizes) { Vector3 childPosition = Vector3.zero; childPosition[flexAxis] = nextChildPosition + flexDirection * childSize[flexAxis] / 2; childPosition[otherAxes.Item1] = Math.Align( childSize, line.Size, otherAxes.Item1, innerAlign[otherAxes.Item1]); childPosition[otherAxes.Item2] = Math.Align( childSize, line.Size, otherAxes.Item2, innerAlign[otherAxes.Item2]); line.ChildPositions.Add(childPosition); nextChildPosition += flexDirection * (childSize[flexAxis] + lineGap); } } } for (int i = 0; i < _lines.Count; i++) { for (int j = 0; j < _lines[i].ChildPositions.Count; j++) { FlexalonLog.Log("FlexArrange | Child Size", _lines[i].Children[j], _lines[i].ChildSizes[j]); FlexalonLog.Log("FlexArrange | Child Position", _lines[i].Children[j], _lines[i].ChildPositions[j]); } } Vector3 totalLineSize = MeasureTotalLineSize(wrap, flexAxis, wrapAxis, thirdAxis); FlexalonLog.Log("FlexArrange | Total Line Size", node, totalLineSize); // Position lines in total line size, consider: totalLineSize, innerAlign { if (wrap) { float wrapGap = 0; if (_lines.Count > 1) { switch (_wrapGapType) { case GapOptions.Fixed: wrapGap = _wrapGap; break; case GapOptions.SpaceBetween: wrapGap = (layoutSize[wrapAxis] - totalLineSize[wrapAxis]) / (_lines.Count - 1); totalLineSize[wrapAxis] = layoutSize[wrapAxis]; break; } } float nextLinePosition = wrapDirection * -totalLineSize[wrapAxis] / 2; foreach (var line in _lines) { line.Position[wrapAxis] = nextLinePosition + wrapDirection * line.Size[wrapAxis] / 2; line.Position[flexAxis] = Math.Align( line.Size, totalLineSize, flexAxis, innerAlign[flexAxis]); line.Position[thirdAxis] = Math.Align( line.Size, totalLineSize, thirdAxis, innerAlign[thirdAxis]); nextLinePosition += wrapDirection * line.Size[wrapAxis] + wrapGap * wrapDirection; } } else { for (int axis = 0; axis < 3; axis++) { _lines[0].Position[axis] = Math.Align( _lines[0].Size, totalLineSize, axis, innerAlign[axis]); } } } for (int i = 0; i < _lines.Count; i++) { FlexalonLog.Log("FlexArrange | Line position " + i + " " + _lines[i].Position); } // Align the total line size within the size Vector3 alignOffset = Vector3.zero; for (int axis = 0; axis < 3; axis++) { alignOffset[axis] = Math.Align(totalLineSize, layoutSize, axis, align[axis]); } FlexalonLog.Log("FlexArrange | alignOffset", node, alignOffset); // Assign final child positions int childIndex = 0; foreach (var line in _lines) { foreach (var childPosition in line.ChildPositions) { var child = node.Children[childIndex]; var result = alignOffset + line.Position + childPosition; child.SetPositionResult(result); child.SetRotationResult(Quaternion.identity); FlexalonLog.Log("FlexArrange | FinalChildPosition", child, result); childIndex++; } } _lines.Clear(); } } }