349 lines
11 KiB
C#
349 lines
11 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace Com.ForbiddenByte.OSA.Util
|
|
{
|
|
[AddComponentMenu("Layout/Packed Grid Layout Group", 153)]
|
|
/// <summary>
|
|
/// Layout class to arrange children elements in a circular grid format. Circular in the . Items will try to occupy as much space as possible.
|
|
/// </summary>
|
|
public class PackedGridLayoutGroup : LayoutGroup
|
|
{
|
|
[SerializeField] protected float m_ForcedSpacing = 0f;
|
|
|
|
[Tooltip("The specified axis will have the 'Preferred' size set based on children")]
|
|
[SerializeField] protected AxisOrNone m_childrenControlSize = AxisOrNone.Vertical;
|
|
|
|
[Tooltip("If true, the layout will start with bigger children. The starting position is defined by the 'Child Alignment' property")]
|
|
[SerializeField] protected bool m_biggerChildrenFirst = true;
|
|
|
|
[Tooltip("Set to as many as possible, if the FPS allows")]
|
|
[Range(1, (int)Packer2DBox.NodeChoosingStrategy.COUNT_)]
|
|
[SerializeField] protected int m_numPasses = (int)Packer2DBox.NodeChoosingStrategy.COUNT_;
|
|
|
|
/// <summary>
|
|
/// The spacing to use between layout elements in the grid on both axes.
|
|
/// The spacing is created by shrinking the childrens' sizes rather than actually adding spaces.
|
|
/// If you want true spacing, consider modifying the children themselves to also include some padding inside them
|
|
/// </summary>
|
|
public float ForcedSpacing { get { return m_ForcedSpacing; } set { SetProperty(ref m_ForcedSpacing, value); } }
|
|
|
|
/// <summary>
|
|
/// The specified axis will have the 'Preferred' size set based on children
|
|
/// </summary>
|
|
public AxisOrNone ChildrenControlSize { get { return m_childrenControlSize; } set { SetProperty(ref m_childrenControlSize, value); } }
|
|
|
|
/// <summary>
|
|
/// If true, the layout will start with bigger children. The starting position is defined by the 'Child Alignment' property
|
|
/// </summary>
|
|
public bool BiggerChildrenFirst { get { return m_biggerChildrenFirst; } set { SetProperty(ref m_biggerChildrenFirst, value); } }
|
|
|
|
/// <summary>
|
|
/// <para>This refers to the number of different strategies to use when packing children. Set to as many as possible, if the FPS allows.
|
|
/// See <see cref="Packer2DBox.NodeChoosingStrategy"/>.</para>
|
|
/// <para> At the moment (15 Mar 2019), more than 1 pass is executing only if the boxes don't all fit in the available
|
|
/// space, as the first strategy (<see cref="Packer2DBox.NodeChoosingStrategy.MAX_VOLUME"/>) seems to always perform the best</para>
|
|
/// </summary>
|
|
public int NumPasses { get { return m_numPasses; } set { SetProperty(ref m_numPasses, value); } }
|
|
|
|
|
|
protected PackedGridLayoutGroup()
|
|
{ }
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
protected override void OnValidate()
|
|
{
|
|
base.OnValidate();
|
|
}
|
|
#endif
|
|
public override void CalculateLayoutInputHorizontal()
|
|
{
|
|
base.CalculateLayoutInputHorizontal();
|
|
|
|
float minWidthToSet;
|
|
float preferredWidthToSet;
|
|
if (m_childrenControlSize == AxisOrNone.Horizontal)
|
|
{
|
|
float width, _;
|
|
GetChildSetups(out width, out _);
|
|
|
|
minWidthToSet = preferredWidthToSet = width + padding.horizontal;
|
|
}
|
|
else
|
|
{
|
|
minWidthToSet = minWidth;
|
|
preferredWidthToSet = preferredWidth;
|
|
}
|
|
|
|
SetLayoutInputForAxis(minWidthToSet, preferredWidthToSet, -1, 0);
|
|
}
|
|
|
|
public override void CalculateLayoutInputVertical()
|
|
{
|
|
float minHeightToSet;
|
|
float preferredHeightToSet;
|
|
if (m_childrenControlSize == AxisOrNone.Vertical)
|
|
{
|
|
float _, height;
|
|
GetChildSetups(out _, out height);
|
|
|
|
minHeightToSet = preferredHeightToSet = height + padding.vertical;
|
|
}
|
|
else
|
|
{
|
|
minHeightToSet = minHeight;
|
|
preferredHeightToSet = preferredHeight;
|
|
}
|
|
|
|
SetLayoutInputForAxis(minHeightToSet, preferredHeightToSet, -1, 1);
|
|
}
|
|
|
|
public override void SetLayoutHorizontal()
|
|
{
|
|
LayoutChildren(true, false);
|
|
}
|
|
|
|
public override void SetLayoutVertical()
|
|
{
|
|
LayoutChildren(false, true);
|
|
}
|
|
|
|
void GetInsetAndEdges(float chWidth, float chHeight, out RectTransform.Edge xInsetEdge, out RectTransform.Edge yInsetEdge, out float xAddInset, out float yAddInset)
|
|
{
|
|
var gridRect = rectTransform.rect;
|
|
|
|
xInsetEdge = RectTransform.Edge.Left;
|
|
yInsetEdge = RectTransform.Edge.Top;
|
|
xAddInset = 0f;
|
|
yAddInset = 0f;
|
|
if (m_childrenControlSize != AxisOrNone.Horizontal)
|
|
{
|
|
switch (childAlignment)
|
|
{
|
|
case TextAnchor.UpperLeft:
|
|
case TextAnchor.LowerLeft:
|
|
case TextAnchor.MiddleLeft:
|
|
xInsetEdge = RectTransform.Edge.Left;
|
|
xAddInset = padding.left;
|
|
break;
|
|
|
|
case TextAnchor.UpperRight:
|
|
case TextAnchor.LowerRight:
|
|
case TextAnchor.MiddleRight:
|
|
xInsetEdge = RectTransform.Edge.Right;
|
|
xAddInset = padding.right;
|
|
break;
|
|
|
|
case TextAnchor.UpperCenter:
|
|
case TextAnchor.MiddleCenter:
|
|
case TextAnchor.LowerCenter:
|
|
xAddInset = Mathf.Max((gridRect.width - chWidth), padding.horizontal) / 2f;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (m_childrenControlSize != AxisOrNone.Vertical)
|
|
{
|
|
switch (childAlignment)
|
|
{
|
|
case TextAnchor.UpperLeft:
|
|
case TextAnchor.UpperCenter:
|
|
case TextAnchor.UpperRight:
|
|
yInsetEdge = RectTransform.Edge.Top;
|
|
yAddInset = padding.top;
|
|
break;
|
|
|
|
case TextAnchor.LowerLeft:
|
|
case TextAnchor.LowerCenter:
|
|
case TextAnchor.LowerRight:
|
|
yInsetEdge = RectTransform.Edge.Bottom;
|
|
yAddInset = padding.bottom;
|
|
break;
|
|
|
|
case TextAnchor.MiddleLeft:
|
|
case TextAnchor.MiddleCenter:
|
|
case TextAnchor.MiddleRight:
|
|
yAddInset = Mathf.Max((gridRect.height - chHeight), padding.vertical) / 2f;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void LayoutChildren(bool hor, bool vert)
|
|
{
|
|
float chWidth, chHeight;
|
|
var setups = GetChildSetups(out chWidth, out chHeight);
|
|
|
|
RectTransform.Edge xInsetEdge, yInsetEdge;
|
|
float xAddInset, yAddInset;
|
|
GetInsetAndEdges(chWidth, chHeight, out xInsetEdge, out yInsetEdge, out xAddInset, out yAddInset);
|
|
|
|
foreach (var child in setups)
|
|
{
|
|
var c = child as ChildSetup;
|
|
if (c.box.position == null)
|
|
continue;
|
|
|
|
if (hor)
|
|
c.child.SetInsetAndSizeFromParentEdge(xInsetEdge, (float)c.box.position.x + xAddInset, (float)c.box.width - ForcedSpacing);
|
|
|
|
if (vert)
|
|
c.child.SetInsetAndSizeFromParentEdge(yInsetEdge, (float)c.box.position.y + yAddInset, (float)c.box.height - ForcedSpacing);
|
|
}
|
|
}
|
|
|
|
List<ChildSetup> GetChildSetups(out float width, out float height)
|
|
{
|
|
var list = new List<ChildSetup>(rectChildren.Count);
|
|
foreach (var child in rectChildren)
|
|
{
|
|
float chWidth = LayoutUtility.GetPreferredSize(child, 0);
|
|
float chHeight = LayoutUtility.GetPreferredSize(child, 1);
|
|
list.Add(new ChildSetup(chWidth, chHeight, child));
|
|
}
|
|
|
|
if (BiggerChildrenFirst)
|
|
{
|
|
// Biggest boxes first with maxside, then secondarily by volume
|
|
// More info: https://codeincomplete.com/posts/bin-packing/
|
|
list.Sort((a, b) =>
|
|
{
|
|
var aMax = System.Math.Max(a.box.width, a.box.height);
|
|
var bMax = System.Math.Max(b.box.width, b.box.height);
|
|
|
|
if (aMax != bMax)
|
|
return (int)(bMax - aMax);
|
|
|
|
return (int)(b.box.volume - a.box.volume);
|
|
});
|
|
}
|
|
|
|
float availableWidth, availableHeight;
|
|
if (m_childrenControlSize == AxisOrNone.Horizontal)
|
|
{
|
|
availableWidth = float.MaxValue;
|
|
availableHeight = rectTransform.rect.height - padding.vertical;
|
|
}
|
|
else if (m_childrenControlSize == AxisOrNone.Vertical)
|
|
{
|
|
availableWidth = rectTransform.rect.width - padding.horizontal;
|
|
availableHeight = float.MaxValue;
|
|
}
|
|
else
|
|
{
|
|
availableHeight = rectTransform.rect.height - padding.vertical;
|
|
availableWidth = rectTransform.rect.width - padding.horizontal;
|
|
}
|
|
|
|
|
|
// Spacing usually creates mode big empty spaces where no item can fit
|
|
float spacingToUse = 0f;
|
|
var packer = new Packer2DBox(availableWidth, availableHeight, spacingToUse);
|
|
|
|
int maxStrategiesToUse = m_numPasses;
|
|
List<Packer2DBox.Box>[] boxesPerStrategy = new List<Packer2DBox.Box>[maxStrategiesToUse];
|
|
int[] nullPositionsPerStrategy = new int[maxStrategiesToUse];
|
|
double[] totalWidthsPerStrategy = new double[maxStrategiesToUse];
|
|
double[] totalHeightsPerStrategy = new double[maxStrategiesToUse];
|
|
|
|
int iBest = -1;
|
|
bool copyBoxesForFinalResult = maxStrategiesToUse > 1;
|
|
for (int i = 0; i < maxStrategiesToUse; i++)
|
|
{
|
|
var boxesThisPass = i == 0 ? list.ConvertAll(c => c.box) : list.ConvertAll(c => { c.ReinitBox(); return c.box; });
|
|
double totalWidthThisPass;
|
|
double totalHeightThisPass;
|
|
packer.Pack(boxesThisPass, false, (Packer2DBox.NodeChoosingStrategy)i, out totalWidthThisPass, out totalHeightThisPass);
|
|
int thisPassNullPositions = 0;
|
|
for (int j = 0; j < boxesThisPass.Count; j++)
|
|
{
|
|
var b = boxesThisPass[j];
|
|
if (b.position == null)
|
|
++thisPassNullPositions;
|
|
}
|
|
|
|
boxesPerStrategy[i] = boxesThisPass;
|
|
nullPositionsPerStrategy[i] = thisPassNullPositions;
|
|
totalWidthsPerStrategy[i] = totalWidthThisPass;
|
|
totalHeightsPerStrategy[i] = totalHeightThisPass;
|
|
|
|
if (iBest == -1)
|
|
{
|
|
iBest = i;
|
|
|
|
if (thisPassNullPositions == 0)
|
|
{
|
|
// Boxes won't be overridden by next strategies, so no need to copy them
|
|
copyBoxesForFinalResult = false;
|
|
|
|
// First pass is Packer2DBox.NodeChoosingStrategy.MAX_VOLUME and all boxes were fit => there's no better strategy
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (thisPassNullPositions < nullPositionsPerStrategy[iBest])
|
|
{
|
|
iBest = i;
|
|
continue;
|
|
}
|
|
|
|
if (thisPassNullPositions > nullPositionsPerStrategy[iBest])
|
|
continue;
|
|
|
|
if (totalWidthThisPass * totalHeightThisPass < totalWidthsPerStrategy[i] * totalHeightsPerStrategy[i])
|
|
{
|
|
iBest = i;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (copyBoxesForFinalResult)
|
|
{
|
|
var bestBoxes = boxesPerStrategy[iBest];
|
|
for (int i = 0; i < list.Count; i++)
|
|
list[i].box = bestBoxes[i];
|
|
}
|
|
|
|
////Testing whether first strategy is always better when no nulls are found
|
|
//if (nullPositionsPerStrategy[0] == 0 && iBest != 0)
|
|
// throw new System.Exception(nullPositionsPerStrategy[0] + ", " + (Packer2DBox.NodeChoosingStrategy)iBest + ", " + nullPositionsPerStrategy[iBest]);
|
|
|
|
//Debug.Log("Strategy used: " + (Packer2DBox.NodeChoosingStrategy)iBest);
|
|
|
|
width = (float)totalWidthsPerStrategy[iBest];
|
|
height = (float)totalHeightsPerStrategy[iBest];
|
|
|
|
return list;
|
|
}
|
|
|
|
|
|
public enum AxisOrNone
|
|
{
|
|
Horizontal = RectTransform.Axis.Horizontal,
|
|
Vertical = RectTransform.Axis.Vertical,
|
|
None
|
|
}
|
|
|
|
|
|
class ChildSetup
|
|
{
|
|
public RectTransform child;
|
|
public Packer2DBox.Box box;
|
|
|
|
|
|
public ChildSetup(double width, double height, RectTransform child)
|
|
{
|
|
this.child = child;
|
|
|
|
box = new Packer2DBox.Box(width, height);
|
|
}
|
|
|
|
|
|
public void ReinitBox() { box = new Packer2DBox.Box(box.width, box.height); }
|
|
}
|
|
}
|
|
} |