Skip to content

Instantly share code, notes, and snippets.

@codorizzi
Last active April 30, 2024 20:57
Show Gist options
  • Star 37 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save codorizzi/79aab1ae7d7940fe3e3603af61cd8617 to your computer and use it in GitHub Desktop.
Save codorizzi/79aab1ae7d7940fe3e3603af61cd8617 to your computer and use it in GitHub Desktop.
Unity - Smooth Layout Group (using DoTween)
using UnityEditor;
using UnityEngine;
namespace Utility.SLayout {
[CustomEditor(typeof(SHorizontalOrVerticalLayoutGroup), true)]
[CanEditMultipleObjects]
/// <summary>
/// Custom Editor for the HorizontalOrVerticalLayoutGroupEditor Component.
/// Extend this class to write a custom editor for a component derived from HorizontalOrVerticalLayoutGroupEditor.
/// </summary>
public class HorizontalOrVerticalLayoutGroupEditor : Editor
{
SerializedProperty moveDuration;
SerializedProperty m_Padding;
SerializedProperty m_Spacing;
SerializedProperty m_ChildAlignment;
SerializedProperty m_ChildControlWidth;
SerializedProperty m_ChildControlHeight;
SerializedProperty m_ChildScaleWidth;
SerializedProperty m_ChildScaleHeight;
SerializedProperty m_ChildForceExpandWidth;
SerializedProperty m_ChildForceExpandHeight;
SerializedProperty m_ReverseArrangement;
protected virtual void OnEnable()
{
moveDuration = serializedObject.FindProperty("moveDuration");
m_Padding = serializedObject.FindProperty("m_Padding");
m_Spacing = serializedObject.FindProperty("m_Spacing");
m_ChildAlignment = serializedObject.FindProperty("m_ChildAlignment");
m_ChildControlWidth = serializedObject.FindProperty("m_ChildControlWidth");
m_ChildControlHeight = serializedObject.FindProperty("m_ChildControlHeight");
m_ChildScaleWidth = serializedObject.FindProperty("m_ChildScaleWidth");
m_ChildScaleHeight = serializedObject.FindProperty("m_ChildScaleHeight");
m_ChildForceExpandWidth = serializedObject.FindProperty("m_ChildForceExpandWidth");
m_ChildForceExpandHeight = serializedObject.FindProperty("m_ChildForceExpandHeight");
m_ReverseArrangement = serializedObject.FindProperty("m_ReverseArrangement");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(moveDuration, true);
EditorGUILayout.PropertyField(m_Padding, true);
EditorGUILayout.PropertyField(m_Spacing, true);
EditorGUILayout.PropertyField(m_ChildAlignment, true);
EditorGUILayout.PropertyField(m_ReverseArrangement, true);
Rect rect = EditorGUILayout.GetControlRect();
rect = EditorGUI.PrefixLabel(rect, -1, EditorGUIUtility.TrTextContent("Control Child Size"));
rect.width = Mathf.Max(50, (rect.width - 4) / 3);
EditorGUIUtility.labelWidth = 50;
ToggleLeft(rect, m_ChildControlWidth, EditorGUIUtility.TrTextContent("Width"));
rect.x += rect.width + 2;
ToggleLeft(rect, m_ChildControlHeight, EditorGUIUtility.TrTextContent("Height"));
EditorGUIUtility.labelWidth = 0;
rect = EditorGUILayout.GetControlRect();
rect = EditorGUI.PrefixLabel(rect, -1, EditorGUIUtility.TrTextContent("Use Child Scale"));
rect.width = Mathf.Max(50, (rect.width - 4) / 3);
EditorGUIUtility.labelWidth = 50;
ToggleLeft(rect, m_ChildScaleWidth, EditorGUIUtility.TrTextContent("Width"));
rect.x += rect.width + 2;
ToggleLeft(rect, m_ChildScaleHeight, EditorGUIUtility.TrTextContent("Height"));
EditorGUIUtility.labelWidth = 0;
rect = EditorGUILayout.GetControlRect();
rect = EditorGUI.PrefixLabel(rect, -1, EditorGUIUtility.TrTextContent("Child Force Expand"));
rect.width = Mathf.Max(50, (rect.width - 4) / 3);
EditorGUIUtility.labelWidth = 50;
ToggleLeft(rect, m_ChildForceExpandWidth, EditorGUIUtility.TrTextContent("Width"));
rect.x += rect.width + 2;
ToggleLeft(rect, m_ChildForceExpandHeight, EditorGUIUtility.TrTextContent("Height"));
EditorGUIUtility.labelWidth = 0;
serializedObject.ApplyModifiedProperties();
}
void ToggleLeft(Rect position, SerializedProperty property, GUIContent label)
{
bool toggle = property.boolValue;
EditorGUI.BeginProperty(position, label, property);
EditorGUI.BeginChangeCheck();
int oldIndent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
toggle = EditorGUI.ToggleLeft(position, label, toggle);
EditorGUI.indentLevel = oldIndent;
if (EditorGUI.EndChangeCheck())
{
property.boolValue = property.hasMultipleDifferentValues ? true : !property.boolValue;
}
EditorGUI.EndProperty();
}
}
}
using UnityEngine;
namespace Utility.SLayout
{
[AddComponentMenu("Layout/SGrid Layout Group", 152)]
public class SGridLayoutGroup : SLayoutGroup
{
/// <summary>
/// Which corner is the starting corner for the grid.
/// </summary>
public enum Corner
{
/// <summary>
/// Upper Left corner.
/// </summary>
UpperLeft = 0,
/// <summary>
/// Upper Right corner.
/// </summary>
UpperRight = 1,
/// <summary>
/// Lower Left corner.
/// </summary>
LowerLeft = 2,
/// <summary>
/// Lower Right corner.
/// </summary>
LowerRight = 3
}
/// <summary>
/// The grid axis we are looking at.
/// </summary>
/// <remarks>
/// As the storage is a [][] we make access easier by passing a axis.
/// </remarks>
public enum Axis
{
/// <summary>
/// Horizontal axis
/// </summary>
Horizontal = 0,
/// <summary>
/// Vertical axis.
/// </summary>
Vertical = 1
}
/// <summary>
/// Constraint type on either the number of columns or rows.
/// </summary>
public enum Constraint
{
/// <summary>
/// Don't constrain the number of rows or columns.
/// </summary>
Flexible = 0,
/// <summary>
/// Constrain the number of columns to a specified number.
/// </summary>
FixedColumnCount = 1,
/// <summary>
/// Constraint the number of rows to a specified number.
/// </summary>
FixedRowCount = 2
}
[SerializeField] protected Corner m_StartCorner = Corner.UpperLeft;
/// <summary>
/// Which corner should the first cell be placed in?
/// </summary>
public Corner startCorner { get { return m_StartCorner; } set { SetProperty(ref m_StartCorner, value); } }
[SerializeField] protected Axis m_StartAxis = Axis.Horizontal;
/// <summary>
/// Which axis should cells be placed along first
/// </summary>
/// <remarks>
/// When startAxis is set to horizontal, an entire row will be filled out before proceeding to the next row. When set to vertical, an entire column will be filled out before proceeding to the next column.
/// </remarks>
public Axis startAxis { get { return m_StartAxis; } set { SetProperty(ref m_StartAxis, value); } }
[SerializeField] protected Vector2 m_CellSize = new Vector2(100, 100);
/// <summary>
/// The size to use for each cell in the grid.
/// </summary>
public Vector2 cellSize { get { return m_CellSize; } set { SetProperty(ref m_CellSize, value); } }
[SerializeField] protected Vector2 m_Spacing = Vector2.zero;
/// <summary>
/// The spacing to use between layout elements in the grid on both axises.
/// </summary>
public Vector2 spacing { get { return m_Spacing; } set { SetProperty(ref m_Spacing, value); } }
[SerializeField] protected Constraint m_Constraint = Constraint.Flexible;
/// <summary>
/// Which constraint to use for the GridLayoutGroup.
/// </summary>
/// <remarks>
/// Specifying a constraint can make the GridLayoutGroup work better in conjunction with a [[ContentSizeFitter]] component. When GridLayoutGroup is used on a RectTransform with a manually specified size, there's no need to specify a constraint.
/// </remarks>
public Constraint constraint { get { return m_Constraint; } set { SetProperty(ref m_Constraint, value); } }
[SerializeField] protected int m_ConstraintCount = 2;
/// <summary>
/// How many cells there should be along the constrained axis.
/// </summary>
public int constraintCount { get { return m_ConstraintCount; } set { SetProperty(ref m_ConstraintCount, Mathf.Max(1, value)); } }
protected SGridLayoutGroup()
{}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
constraintCount = constraintCount;
}
#endif
/// <summary>
/// Called by the layout system to calculate the horizontal layout size.
/// Also see ILayoutElement
/// </summary>
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
int minColumns = 0;
int preferredColumns = 0;
if (m_Constraint == Constraint.FixedColumnCount)
{
minColumns = preferredColumns = m_ConstraintCount;
}
else if (m_Constraint == Constraint.FixedRowCount)
{
minColumns = preferredColumns = Mathf.CeilToInt(rectChildren.Count / (float)m_ConstraintCount - 0.001f);
}
else
{
minColumns = 1;
preferredColumns = Mathf.CeilToInt(Mathf.Sqrt(rectChildren.Count));
}
SetLayoutInputForAxis(
padding.horizontal + (cellSize.x + spacing.x) * minColumns - spacing.x,
padding.horizontal + (cellSize.x + spacing.x) * preferredColumns - spacing.x,
-1, 0);
}
/// <summary>
/// Called by the layout system to calculate the vertical layout size.
/// Also see ILayoutElement
/// </summary>
public override void CalculateLayoutInputVertical()
{
int minRows = 0;
if (m_Constraint == Constraint.FixedColumnCount)
{
minRows = Mathf.CeilToInt(rectChildren.Count / (float)m_ConstraintCount - 0.001f);
}
else if (m_Constraint == Constraint.FixedRowCount)
{
minRows = m_ConstraintCount;
}
else
{
float width = rectTransform.rect.width;
int cellCountX = Mathf.Max(1, Mathf.FloorToInt((width - padding.horizontal + spacing.x + 0.001f) / (cellSize.x + spacing.x)));
minRows = Mathf.CeilToInt(rectChildren.Count / (float)cellCountX);
}
float minSpace = padding.vertical + (cellSize.y + spacing.y) * minRows - spacing.y;
SetLayoutInputForAxis(minSpace, minSpace, -1, 1);
}
/// <summary>
/// Called by the layout system
/// Also see ILayoutElement
/// </summary>
public override void SetLayoutHorizontal()
{
SetCellsAlongAxis(0);
}
/// <summary>
/// Called by the layout system
/// Also see ILayoutElement
/// </summary>
public override void SetLayoutVertical()
{
SetCellsAlongAxis(1);
}
private void SetCellsAlongAxis(int axis)
{
// Normally a Layout Controller should only set horizontal values when invoked for the horizontal axis
// and only vertical values when invoked for the vertical axis.
// However, in this case we set both the horizontal and vertical position when invoked for the vertical axis.
// Since we only set the horizontal position and not the size, it shouldn't affect children's layout,
// and thus shouldn't break the rule that all horizontal layout must be calculated before all vertical layout.
var rectChildrenCount = rectChildren.Count;
if (axis == 0)
{
// Only set the sizes when invoked for horizontal axis, not the positions.
for (int i = 0; i < rectChildrenCount; i++)
{
RectTransform rect = rectChildren[i];
m_Tracker.Add(this, rect,
DrivenTransformProperties.Anchors |
DrivenTransformProperties.AnchoredPosition |
DrivenTransformProperties.SizeDelta);
rect.anchorMin = Vector2.up;
rect.anchorMax = Vector2.up;
rect.sizeDelta = cellSize;
}
return;
}
float width = rectTransform.rect.size.x;
float height = rectTransform.rect.size.y;
int cellCountX = 1;
int cellCountY = 1;
if (m_Constraint == Constraint.FixedColumnCount)
{
cellCountX = m_ConstraintCount;
if (rectChildrenCount > cellCountX)
cellCountY = rectChildrenCount / cellCountX + (rectChildrenCount % cellCountX > 0 ? 1 : 0);
}
else if (m_Constraint == Constraint.FixedRowCount)
{
cellCountY = m_ConstraintCount;
if (rectChildrenCount > cellCountY)
cellCountX = rectChildrenCount / cellCountY + (rectChildrenCount % cellCountY > 0 ? 1 : 0);
}
else
{
if (cellSize.x + spacing.x <= 0)
cellCountX = int.MaxValue;
else
cellCountX = Mathf.Max(1, Mathf.FloorToInt((width - padding.horizontal + spacing.x + 0.001f) / (cellSize.x + spacing.x)));
if (cellSize.y + spacing.y <= 0)
cellCountY = int.MaxValue;
else
cellCountY = Mathf.Max(1, Mathf.FloorToInt((height - padding.vertical + spacing.y + 0.001f) / (cellSize.y + spacing.y)));
}
int cornerX = (int)startCorner % 2;
int cornerY = (int)startCorner / 2;
int cellsPerMainAxis, actualCellCountX, actualCellCountY;
if (startAxis == Axis.Horizontal)
{
cellsPerMainAxis = cellCountX;
actualCellCountX = Mathf.Clamp(cellCountX, 1, rectChildrenCount);
actualCellCountY = Mathf.Clamp(cellCountY, 1, Mathf.CeilToInt(rectChildrenCount / (float)cellsPerMainAxis));
}
else
{
cellsPerMainAxis = cellCountY;
actualCellCountY = Mathf.Clamp(cellCountY, 1, rectChildrenCount);
actualCellCountX = Mathf.Clamp(cellCountX, 1, Mathf.CeilToInt(rectChildrenCount / (float)cellsPerMainAxis));
}
Vector2 requiredSpace = new Vector2(
actualCellCountX * cellSize.x + (actualCellCountX - 1) * spacing.x,
actualCellCountY * cellSize.y + (actualCellCountY - 1) * spacing.y
);
Vector2 startOffset = new Vector2(
GetStartOffset(0, requiredSpace.x),
GetStartOffset(1, requiredSpace.y)
);
for (int i = 0; i < rectChildrenCount; i++)
{
int positionX;
int positionY;
if (startAxis == Axis.Horizontal)
{
positionX = i % cellsPerMainAxis;
positionY = i / cellsPerMainAxis;
}
else
{
positionX = i / cellsPerMainAxis;
positionY = i % cellsPerMainAxis;
}
if (cornerX == 1)
positionX = actualCellCountX - 1 - positionX;
if (cornerY == 1)
positionY = actualCellCountY - 1 - positionY;
SetChildAlongAxis(rectChildren[i], 0, startOffset.x + (cellSize[0] + spacing[0]) * positionX, cellSize[0]);
SetChildAlongAxis(rectChildren[i], 1, startOffset.y + (cellSize[1] + spacing[1]) * positionY, cellSize[1]);
}
}
}
}
using UnityEngine;
namespace Utility.SLayout
{
[AddComponentMenu("Layout/SHorizontal Layout Group", 150)]
public class SHorizontalLayoutGroup : SHorizontalOrVerticalLayoutGroup
{
protected SHorizontalLayoutGroup()
{}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
CalcAlongAxis(0, false);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void CalculateLayoutInputVertical()
{
CalcAlongAxis(1, false);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void SetLayoutHorizontal()
{
SetChildrenAlongAxis(0, false);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void SetLayoutVertical()
{
SetChildrenAlongAxis(1, false);
}
public void ForceUpdate() {
SetDirty();
}
}
}
using UnityEngine;
using UnityEngine.UI;
namespace Utility.SLayout
{
/// <summary>
/// Abstract base class for HorizontalLayoutGroup and VerticalLayoutGroup to generalize common functionality.
/// </summary>
///
[ExecuteAlways]
public abstract class SHorizontalOrVerticalLayoutGroup : SLayoutGroup
{
[SerializeField] protected float m_Spacing = 0;
/// <summary>
/// The spacing to use between layout elements in the layout group.
/// </summary>
public float spacing { get { return m_Spacing; } set { SetProperty(ref m_Spacing, value); } }
[SerializeField] protected bool m_ChildForceExpandWidth = true;
/// <summary>
/// Whether to force the children to expand to fill additional available horizontal space.
/// </summary>
public bool childForceExpandWidth { get { return m_ChildForceExpandWidth; } set { SetProperty(ref m_ChildForceExpandWidth, value); } }
[SerializeField] protected bool m_ChildForceExpandHeight = true;
/// <summary>
/// Whether to force the children to expand to fill additional available vertical space.
/// </summary>
public bool childForceExpandHeight { get { return m_ChildForceExpandHeight; } set { SetProperty(ref m_ChildForceExpandHeight, value); } }
[SerializeField] protected bool m_ChildControlWidth = true;
/// <summary>
/// Returns true if the Layout Group controls the widths of its children. Returns false if children control their own widths.
/// </summary>
/// <remarks>
/// If set to false, the layout group will only affect the positions of the children while leaving the widths untouched. The widths of the children can be set via the respective RectTransforms in this case.
///
/// If set to true, the widths of the children are automatically driven by the layout group according to their respective minimum, preferred, and flexible widths. This is useful if the widths of the children should change depending on how much space is available.In this case the width of each child cannot be set manually in the RectTransform, but the minimum, preferred and flexible width for each child can be controlled by adding a LayoutElement component to it.
/// </remarks>
public bool childControlWidth { get { return m_ChildControlWidth; } set { SetProperty(ref m_ChildControlWidth, value); } }
[SerializeField] protected bool m_ChildControlHeight = true;
/// <summary>
/// Returns true if the Layout Group controls the heights of its children. Returns false if children control their own heights.
/// </summary>
/// <remarks>
/// If set to false, the layout group will only affect the positions of the children while leaving the heights untouched. The heights of the children can be set via the respective RectTransforms in this case.
///
/// If set to true, the heights of the children are automatically driven by the layout group according to their respective minimum, preferred, and flexible heights. This is useful if the heights of the children should change depending on how much space is available.In this case the height of each child cannot be set manually in the RectTransform, but the minimum, preferred and flexible height for each child can be controlled by adding a LayoutElement component to it.
/// </remarks>
public bool childControlHeight { get { return m_ChildControlHeight; } set { SetProperty(ref m_ChildControlHeight, value); } }
[SerializeField] protected bool m_ChildScaleWidth = false;
/// <summary>
/// Whether to use the x scale of each child when calculating its width.
/// </summary>
public bool childScaleWidth { get { return m_ChildScaleWidth; } set { SetProperty(ref m_ChildScaleWidth, value); } }
[SerializeField] protected bool m_ChildScaleHeight = false;
/// <summary>
/// Whether to use the y scale of each child when calculating its height.
/// </summary>
public bool childScaleHeight { get { return m_ChildScaleHeight; } set { SetProperty(ref m_ChildScaleHeight, value); } }
/// <summary>
/// Whether the order of children objects should be sorted in reverse.
/// </summary>
/// <remarks>
/// If False the first child object will be positioned first.
/// If True the last child object will be positioned first.
/// </remarks>
public bool reverseArrangement { get { return m_ReverseArrangement; } set { SetProperty(ref m_ReverseArrangement, value); } }
[SerializeField] protected bool m_ReverseArrangement = false;
/// <summary>
/// Calculate the layout element properties for this layout element along the given axis.
/// </summary>
/// <param name="axis">The axis to calculate for. 0 is horizontal and 1 is vertical.</param>
/// <param name="isVertical">Is this group a vertical group?</param>
protected void CalcAlongAxis(int axis, bool isVertical)
{
float combinedPadding = (axis == 0 ? padding.horizontal : padding.vertical);
bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
bool useScale = (axis == 0 ? m_ChildScaleWidth : m_ChildScaleHeight);
bool childForceExpandSize = (axis == 0 ? m_ChildForceExpandWidth : m_ChildForceExpandHeight);
float totalMin = combinedPadding;
float totalPreferred = combinedPadding;
float totalFlexible = 0;
bool alongOtherAxis = (isVertical ^ (axis == 1));
var rectChildrenCount = rectChildren.Count;
for (int i = 0; i < rectChildrenCount; i++)
{
RectTransform child = rectChildren[i];
float min, preferred, flexible;
GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
if (useScale)
{
float scaleFactor = child.localScale[axis];
min *= scaleFactor;
preferred *= scaleFactor;
flexible *= scaleFactor;
}
if (alongOtherAxis)
{
totalMin = Mathf.Max(min + combinedPadding, totalMin);
totalPreferred = Mathf.Max(preferred + combinedPadding, totalPreferred);
totalFlexible = Mathf.Max(flexible, totalFlexible);
}
else
{
totalMin += min + spacing;
totalPreferred += preferred + spacing;
// Increment flexible size with element's flexible size.
totalFlexible += flexible;
}
}
if (!alongOtherAxis && rectChildren.Count > 0)
{
totalMin -= spacing;
totalPreferred -= spacing;
}
totalPreferred = Mathf.Max(totalMin, totalPreferred);
SetLayoutInputForAxis(totalMin, totalPreferred, totalFlexible, axis);
}
/// <summary>
/// Set the positions and sizes of the child layout elements for the given axis.
/// </summary>
/// <param name="axis">The axis to handle. 0 is horizontal and 1 is vertical.</param>
/// <param name="isVertical">Is this group a vertical group?</param>
protected void SetChildrenAlongAxis(int axis, bool isVertical)
{
float size = rectTransform.rect.size[axis];
bool controlSize = (axis == 0 ? m_ChildControlWidth : m_ChildControlHeight);
bool useScale = (axis == 0 ? m_ChildScaleWidth : m_ChildScaleHeight);
bool childForceExpandSize = (axis == 0 ? m_ChildForceExpandWidth : m_ChildForceExpandHeight);
float alignmentOnAxis = GetAlignmentOnAxis(axis);
bool alongOtherAxis = (isVertical ^ (axis == 1));
int startIndex = m_ReverseArrangement ? rectChildren.Count - 1 : 0;
int endIndex = m_ReverseArrangement ? 0 : rectChildren.Count;
int increment = m_ReverseArrangement ? -1 : 1;
if (alongOtherAxis)
{
float innerSize = size - (axis == 0 ? padding.horizontal : padding.vertical);
for (int i = startIndex; m_ReverseArrangement ? i >= endIndex : i < endIndex; i += increment)
{
RectTransform child = rectChildren[i];
float min, preferred, flexible;
GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
float scaleFactor = useScale ? child.localScale[axis] : 1f;
float requiredSpace = Mathf.Clamp(innerSize, min, flexible > 0 ? size : preferred);
float startOffset = GetStartOffset(axis, requiredSpace * scaleFactor);
if (controlSize)
{
SetChildAlongAxisWithScale(child, axis, startOffset, requiredSpace, scaleFactor);
}
else
{
float offsetInCell = (requiredSpace - child.sizeDelta[axis]) * alignmentOnAxis;
SetChildAlongAxisWithScale(child, axis, startOffset + offsetInCell, scaleFactor);
}
}
}
else
{
float pos = (axis == 0 ? padding.left : padding.top);
float itemFlexibleMultiplier = 0;
float surplusSpace = size - GetTotalPreferredSize(axis);
if (surplusSpace > 0)
{
if (GetTotalFlexibleSize(axis) == 0)
pos = GetStartOffset(axis, GetTotalPreferredSize(axis) - (axis == 0 ? padding.horizontal : padding.vertical));
else if (GetTotalFlexibleSize(axis) > 0)
itemFlexibleMultiplier = surplusSpace / GetTotalFlexibleSize(axis);
}
float minMaxLerp = 0;
if (GetTotalMinSize(axis) != GetTotalPreferredSize(axis))
minMaxLerp = Mathf.Clamp01((size - GetTotalMinSize(axis)) / (GetTotalPreferredSize(axis) - GetTotalMinSize(axis)));
for (int i = startIndex; m_ReverseArrangement ? i >= endIndex : i < endIndex; i += increment)
{
RectTransform child = rectChildren[i];
float min, preferred, flexible;
GetChildSizes(child, axis, controlSize, childForceExpandSize, out min, out preferred, out flexible);
float scaleFactor = useScale ? child.localScale[axis] : 1f;
float childSize = Mathf.Lerp(min, preferred, minMaxLerp);
childSize += flexible * itemFlexibleMultiplier;
if (controlSize)
{
SetChildAlongAxisWithScale(child, axis, pos, childSize, scaleFactor);
}
else
{
float offsetInCell = (childSize - child.sizeDelta[axis]) * alignmentOnAxis;
SetChildAlongAxisWithScale(child, axis, pos + offsetInCell, scaleFactor);
}
pos += childSize * scaleFactor + spacing;
}
}
}
private void GetChildSizes(RectTransform child, int axis, bool controlSize, bool childForceExpand,
out float min, out float preferred, out float flexible)
{
if (!controlSize)
{
min = child.sizeDelta[axis];
preferred = min;
flexible = 0;
}
else
{
min = LayoutUtility.GetMinSize(child, axis);
preferred = LayoutUtility.GetPreferredSize(child, axis);
flexible = LayoutUtility.GetFlexibleSize(child, axis);
}
if (childForceExpand)
flexible = Mathf.Max(flexible, 1);
}
#if UNITY_EDITOR
protected override void Reset()
{
base.Reset();
// For new added components we want these to be set to false,
// so that the user's sizes won't be overwritten before they
// have a chance to turn these settings off.
// However, for existing components that were added before this
// feature was introduced, we want it to be on be default for
// backwardds compatibility.
// Hence their default value is on, but we set to off in reset.
m_ChildControlWidth = false;
m_ChildControlHeight = false;
}
private int m_Capacity = 10;
private Vector2[] m_Sizes = new Vector2[10];
protected virtual void Update()
{
if (Application.isPlaying)
return;
int count = transform.childCount;
if (count > m_Capacity)
{
if (count > m_Capacity * 2)
m_Capacity = count;
else
m_Capacity *= 2;
m_Sizes = new Vector2[m_Capacity];
}
// If children size change in editor, update layout (case 945680 - Child GameObjects in a Horizontal/Vertical Layout Group don't display their correct position in the Editor)
bool dirty = false;
for (int i = 0; i < count; i++)
{
RectTransform t = transform.GetChild(i) as RectTransform;
if (t != null && t.sizeDelta != m_Sizes[i])
{
dirty = true;
m_Sizes[i] = t.sizeDelta;
}
}
if (dirty)
LayoutRebuilder.MarkLayoutForRebuild(transform as RectTransform);
}
#endif
}
}
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Pool;
using UnityEngine.UI;
namespace Utility.SLayout {
[DisallowMultipleComponent]
[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
public abstract class SLayoutGroup : UIBehaviour, ILayoutElement, ILayoutGroup {
[SerializeField] protected RectOffset m_Padding = new RectOffset();
[SerializeField] public float moveDuration = 1f;
private Dictionary<RectTransform, Tween> RectPositionXTweens = new Dictionary<RectTransform, Tween>();
private Dictionary<RectTransform, Tween> RectPositionYTweens = new Dictionary<RectTransform, Tween>();
/// <summary>
/// The padding to add around the child layout elements.
/// </summary>
public RectOffset padding { get { return m_Padding; } set { SetProperty(ref m_Padding, value); } }
[SerializeField] protected TextAnchor m_ChildAlignment = TextAnchor.UpperLeft;
/// <summary>
/// The alignment to use for the child layout elements in the layout group.
/// </summary>
/// <remarks>
/// If a layout element does not specify a flexible width or height, its child elements many not use the available space within the layout group. In this case, use the alignment settings to specify how to align child elements within their layout group.
/// </remarks>
public TextAnchor childAlignment { get { return m_ChildAlignment; } set { SetProperty(ref m_ChildAlignment, value); } }
[System.NonSerialized] private RectTransform m_Rect;
protected RectTransform rectTransform
{
get
{
if (m_Rect == null)
m_Rect = GetComponent<RectTransform>();
return m_Rect;
}
}
protected DrivenRectTransformTracker m_Tracker;
private Vector2 m_TotalMinSize = Vector2.zero;
private Vector2 m_TotalPreferredSize = Vector2.zero;
private Vector2 m_TotalFlexibleSize = Vector2.zero;
[System.NonSerialized] private List<RectTransform> m_RectChildren = new List<RectTransform>();
protected List<RectTransform> rectChildren { get { return m_RectChildren; } }
public virtual void CalculateLayoutInputHorizontal()
{
m_RectChildren.Clear();
var toIgnoreList = ListPool<Component>.Get();
for (int i = 0; i < rectTransform.childCount; i++)
{
var rect = rectTransform.GetChild(i) as RectTransform;
if (rect == null || !rect.gameObject.activeInHierarchy)
continue;
rect.GetComponents(typeof(ILayoutIgnorer), toIgnoreList);
if (toIgnoreList.Count == 0)
{
m_RectChildren.Add(rect);
continue;
}
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();
}
public abstract void CalculateLayoutInputVertical();
/// <summary>
/// See LayoutElement.minWidth
/// </summary>
public virtual float minWidth { get { return GetTotalMinSize(0); } }
/// <summary>
/// See LayoutElement.preferredWidth
/// </summary>
public virtual float preferredWidth { get { return GetTotalPreferredSize(0); } }
/// <summary>
/// See LayoutElement.flexibleWidth
/// </summary>
public virtual float flexibleWidth { get { return GetTotalFlexibleSize(0); } }
/// <summary>
/// See LayoutElement.minHeight
/// </summary>
public virtual float minHeight { get { return GetTotalMinSize(1); } }
/// <summary>
/// See LayoutElement.preferredHeight
/// </summary>
public virtual float preferredHeight { get { return GetTotalPreferredSize(1); } }
/// <summary>
/// See LayoutElement.flexibleHeight
/// </summary>
public virtual float flexibleHeight { get { return GetTotalFlexibleSize(1); } }
/// <summary>
/// See LayoutElement.layoutPriority
/// </summary>
public virtual int layoutPriority { get { return 0; } }
// ILayoutController Interface
public abstract void SetLayoutHorizontal();
public abstract void SetLayoutVertical();
// Implementation
protected SLayoutGroup()
{
if (m_Padding == null)
m_Padding = new RectOffset();
}
protected override void OnEnable()
{
base.OnEnable();
SetDirty();
}
protected override void OnDisable()
{
m_Tracker.Clear();
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
base.OnDisable();
}
/// <summary>
/// Callback for when properties have been changed by animation.
/// </summary>
protected override void OnDidApplyAnimationProperties()
{
SetDirty();
}
/// <summary>
/// The min size for the layout group on the given axis.
/// </summary>
/// <param name="axis">The axis index. 0 is horizontal and 1 is vertical.</param>
/// <returns>The min size</returns>
protected float GetTotalMinSize(int axis)
{
return m_TotalMinSize[axis];
}
/// <summary>
/// The preferred size for the layout group on the given axis.
/// </summary>
/// <param name="axis">The axis index. 0 is horizontal and 1 is vertical.</param>
/// <returns>The preferred size.</returns>
protected float GetTotalPreferredSize(int axis)
{
return m_TotalPreferredSize[axis];
}
/// <summary>
/// The flexible size for the layout group on the given axis.
/// </summary>
/// <param name="axis">The axis index. 0 is horizontal and 1 is vertical.</param>
/// <returns>The flexible size</returns>
protected float GetTotalFlexibleSize(int axis)
{
return m_TotalFlexibleSize[axis];
}
/// <summary>
/// Returns the calculated position of the first child layout element along the given axis.
/// </summary>
/// <param name="axis">The axis index. 0 is horizontal and 1 is vertical.</param>
/// <param name="requiredSpaceWithoutPadding">The total space required on the given axis for all the layout elements including spacing and excluding padding.</param>
/// <returns>The position of the first child along the given axis.</returns>
protected float GetStartOffset(int axis, float requiredSpaceWithoutPadding)
{
float requiredSpace = requiredSpaceWithoutPadding + (axis == 0 ? padding.horizontal : padding.vertical);
float availableSpace = rectTransform.rect.size[axis];
float surplusSpace = availableSpace - requiredSpace;
float alignmentOnAxis = GetAlignmentOnAxis(axis);
return (axis == 0 ? padding.left : padding.top) + surplusSpace * alignmentOnAxis;
}
/// <summary>
/// Returns the alignment on the specified axis as a fraction where 0 is left/top, 0.5 is middle, and 1 is right/bottom.
/// </summary>
/// <param name="axis">The axis to get alignment along. 0 is horizontal and 1 is vertical.</param>
/// <returns>The alignment as a fraction where 0 is left/top, 0.5 is middle, and 1 is right/bottom.</returns>
protected float GetAlignmentOnAxis(int axis)
{
if (axis == 0)
return ((int)childAlignment % 3) * 0.5f;
else
return ((int)childAlignment / 3) * 0.5f;
}
/// <summary>
/// Used to set the calculated layout properties for the given axis.
/// </summary>
/// <param name="totalMin">The min size for the layout group.</param>
/// <param name="totalPreferred">The preferred size for the layout group.</param>
/// <param name="totalFlexible">The flexible size for the layout group.</param>
/// <param name="axis">The axis to set sizes for. 0 is horizontal and 1 is vertical.</param>
protected void SetLayoutInputForAxis(float totalMin, float totalPreferred, float totalFlexible, int axis)
{
m_TotalMinSize[axis] = totalMin;
m_TotalPreferredSize[axis] = totalPreferred;
m_TotalFlexibleSize[axis] = totalFlexible;
}
/// <summary>
/// Set the position and size of a child layout element along the given axis.
/// </summary>
/// <param name="rect">The RectTransform of the child layout element.</param>
/// <param name="axis">The axis to set the position and size along. 0 is horizontal and 1 is vertical.</param>
/// <param name="pos">The position from the left side or top.</param>
protected void SetChildAlongAxis(RectTransform rect, int axis, float pos)
{
if (rect == null)
return;
SetChildAlongAxisWithScale(rect, axis, pos, 1.0f);
}
/// <summary>
/// Set the position and size of a child layout element along the given axis.
/// </summary>
/// <param name="rect">The RectTransform of the child layout element.</param>
/// <param name="axis">The axis to set the position and size along. 0 is horizontal and 1 is vertical.</param>
/// <param name="pos">The position from the left side or top.</param>
protected void SetChildAlongAxisWithScale(RectTransform rect, int axis, float pos, float scaleFactor)
{
if (rect == null)
return;
m_Tracker.Add(this, rect,
DrivenTransformProperties.Anchors |
(axis == 0 ? DrivenTransformProperties.AnchoredPositionX : DrivenTransformProperties.AnchoredPositionY));
// Inlined rect.SetInsetAndSizeFromParentEdge(...) and refactored code in order to multiply desired size by scaleFactor.
// sizeDelta must stay the same but the size used in the calculation of the position must be scaled by the scaleFactor.
rect.anchorMin = Vector2.up;
rect.anchorMax = Vector2.up;
Vector2 anchoredPosition = rect.anchoredPosition;
anchoredPosition[axis] = (axis == 0) ? (pos + rect.sizeDelta[axis] * rect.pivot[axis] * scaleFactor) : (-pos - rect.sizeDelta[axis] * (1f - rect.pivot[axis]) * scaleFactor);
SetPosition(rect, anchoredPosition, axis);
}
/// <summary>
/// Set the position and size of a child layout element along the given axis.
/// </summary>
/// <param name="rect">The RectTransform of the child layout element.</param>
/// <param name="axis">The axis to set the position and size along. 0 is horizontal and 1 is vertical.</param>
/// <param name="pos">The position from the left side or top.</param>
/// <param name="size">The size.</param>
protected void SetChildAlongAxis(RectTransform rect, int axis, float pos, float size)
{
if (rect == null)
return;
SetChildAlongAxisWithScale(rect, axis, pos, size, 1.0f);
}
/// <summary>
/// Set the position and size of a child layout element along the given axis.
/// </summary>
/// <param name="rect">The RectTransform of the child layout element.</param>
/// <param name="axis">The axis to set the position and size along. 0 is horizontal and 1 is vertical.</param>
/// <param name="pos">The position from the left side or top.</param>
/// <param name="size">The size.</param>
protected void SetChildAlongAxisWithScale(RectTransform rect, int axis, float pos, float size, float scaleFactor)
{
if (rect == null)
return;
m_Tracker.Add(this, rect,
DrivenTransformProperties.Anchors |
(axis == 0 ?
(DrivenTransformProperties.AnchoredPositionX | DrivenTransformProperties.SizeDeltaX) :
(DrivenTransformProperties.AnchoredPositionY | DrivenTransformProperties.SizeDeltaY)
)
);
// Inlined rect.SetInsetAndSizeFromParentEdge(...) and refactored code in order to multiply desired size by scaleFactor.
// sizeDelta must stay the same but the size used in the calculation of the position must be scaled by the scaleFactor.
rect.anchorMin = Vector2.up;
rect.anchorMax = Vector2.up;
Vector2 sizeDelta = rect.sizeDelta;
sizeDelta[axis] = size;
rect.sizeDelta = sizeDelta;
Vector2 anchoredPosition = rect.anchoredPosition;
anchoredPosition[axis] = (axis == 0) ? (pos + size * rect.pivot[axis] * scaleFactor) : (-pos - size * (1f - rect.pivot[axis]) * scaleFactor);
SetPosition(rect, anchoredPosition, axis);
}
private bool isRootLayoutGroup
{
get
{
Transform parent = transform.parent;
if (parent == null)
return true;
return transform.parent.GetComponent(typeof(ILayoutGroup)) == null;
}
}
protected override void OnRectTransformDimensionsChange()
{
base.OnRectTransformDimensionsChange();
if (isRootLayoutGroup)
SetDirty();
}
protected virtual void OnTransformChildrenChanged()
{
SetDirty();
}
/// <summary>
/// Helper method used to set a given property if it has changed.
/// </summary>
/// <param name="currentValue">A reference to the member value.</param>
/// <param name="newValue">The new value.</param>
protected void SetProperty<T>(ref T currentValue, T newValue)
{
if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
return;
currentValue = newValue;
SetDirty();
}
/// <summary>
/// Mark the LayoutGroup as dirty.
/// </summary>
protected void SetDirty()
{
if (!IsActive())
return;
if (!CanvasUpdateRegistry.IsRebuildingLayout())
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
else
StartCoroutine(DelayedSetDirty(rectTransform));
}
IEnumerator DelayedSetDirty(RectTransform rectTransform)
{
yield return null;
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}
public void SetPosition(RectTransform rect, Vector2 pos, int axis) {
if(!Application.isPlaying) {
rect.anchoredPosition = pos;
}
Tween tween = null;
Dictionary<RectTransform, Tween> dict = null;
switch (axis) {
case 0:
dict = RectPositionXTweens;
tween = rect.DOAnchorPosX(pos.x, moveDuration);
break;
case 1:
dict = RectPositionYTweens;
tween = rect.DOAnchorPosY(pos.y, moveDuration);
break;
default:
return;
}
if(dict == null)
dict = new Dictionary<RectTransform, Tween>();
if (dict.Keys.Contains(rect))
dict[rect].Kill();
// cache cleanup
tween.onKill += () => {
if(dict.Keys.Contains(rect) && dict[rect] == tween)
dict.Remove(rect);
};
tween.onComplete += () => {
if (!dict.ContainsKey(rect))
return;
dict.Remove(rect);
};
if (dict.ContainsKey(rect))
dict[rect] = tween;
else {
dict.Add(rect, tween);
}
}
#if UNITY_EDITOR
protected override void OnValidate()
{
SetDirty();
}
#endif
}
}
using UnityEngine;
namespace Utility.SLayout
{
[AddComponentMenu("Layout/SVertical Layout Group", 151)]
public class SVerticalLayoutGroup : SHorizontalOrVerticalLayoutGroup
{
protected SVerticalLayoutGroup()
{}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
CalcAlongAxis(0, true);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void CalculateLayoutInputVertical()
{
CalcAlongAxis(1, true);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void SetLayoutHorizontal()
{
SetChildrenAlongAxis(0, true);
}
/// <summary>
/// Called by the layout system. Also see ILayoutElement
/// </summary>
public override void SetLayoutVertical()
{
SetChildrenAlongAxis(1, true);
}
}
}
@bawahakim
Copy link

Very useful script, thanks @codorizzi !

For those not on 2021+, you can replace the namespace using Unity.Pool to using Unity.Rendering.

For convenience, added a custom editor script that makes the fields look like the original LayoutGroup component (taken from Unity Editor directly). See below.

Note: I've added a field [SerializeField] protected float m_SmoothDuration = 0.5f; in SLayoutGroup to not have a dependency on ActionDash and be able to tweak it in the editor.

	[CanEditMultipleObjects]
	public class SHorizontalOrVerticalLayoutGroupEditor : Editor
	{
		SerializedProperty m_Padding;
		SerializedProperty m_Spacing;
		SerializedProperty m_ChildAlignment;
		SerializedProperty m_ChildForceExpandWidth;
		SerializedProperty m_ChildForceExpandHeight;
		SerializedProperty m_ChildControlWidth;
		SerializedProperty m_ChildControlHeight;
		SerializedProperty m_ChildScaleWidth;
		SerializedProperty m_ChildScaleHeight;
		SerializedProperty m_ReverseArrangement;
		
		SerializedProperty m_SmoothDuration;

		void OnEnable()
		{
			m_Padding = serializedObject.FindProperty("m_Padding");
			m_Spacing = serializedObject.FindProperty("m_Spacing");
			m_ChildAlignment = serializedObject.FindProperty("m_ChildAlignment");
			m_ChildForceExpandWidth = serializedObject.FindProperty("m_ChildForceExpandWidth");
			m_ChildForceExpandHeight = serializedObject.FindProperty("m_ChildForceExpandHeight");
			m_ChildControlWidth = serializedObject.FindProperty("m_ChildControlWidth");
			m_ChildControlHeight = serializedObject.FindProperty("m_ChildControlHeight");
			m_ChildScaleWidth = serializedObject.FindProperty("m_ChildScaleWidth");
			m_ChildScaleHeight = serializedObject.FindProperty("m_ChildScaleHeight");
			m_ReverseArrangement = serializedObject.FindProperty("m_ReverseArrangement");
			m_SmoothDuration = serializedObject.FindProperty("m_SmoothDuration");
		}
 
      public override void OnInspectorGUI()
        {
            serializedObject.Update();
            EditorGUILayout.PropertyField(m_Padding, true);
            EditorGUILayout.PropertyField(m_Spacing, true);
            EditorGUILayout.PropertyField(m_ChildAlignment, true);
            EditorGUILayout.PropertyField(m_ReverseArrangement, true);

            Rect rect = EditorGUILayout.GetControlRect();
            rect = EditorGUI.PrefixLabel(rect, -1, EditorGUIUtility.TrTextContent("Control Child Size"));
            rect.width = Mathf.Max(50, (rect.width - 4) / 3);
            EditorGUIUtility.labelWidth = 50;
            ToggleLeft(rect, m_ChildControlWidth, EditorGUIUtility.TrTextContent("Width"));
            rect.x += rect.width + 2;
            ToggleLeft(rect, m_ChildControlHeight, EditorGUIUtility.TrTextContent("Height"));
            EditorGUIUtility.labelWidth = 0;

            rect = EditorGUILayout.GetControlRect();
            rect = EditorGUI.PrefixLabel(rect, -1, EditorGUIUtility.TrTextContent("Use Child Scale"));
            rect.width = Mathf.Max(50, (rect.width - 4) / 3);
            EditorGUIUtility.labelWidth = 50;
            ToggleLeft(rect, m_ChildScaleWidth, EditorGUIUtility.TrTextContent("Width"));
            rect.x += rect.width + 2;
            ToggleLeft(rect, m_ChildScaleHeight, EditorGUIUtility.TrTextContent("Height"));
            EditorGUIUtility.labelWidth = 0;

            rect = EditorGUILayout.GetControlRect();
            rect = EditorGUI.PrefixLabel(rect, -1, EditorGUIUtility.TrTextContent("Child Force Expand"));
            rect.width = Mathf.Max(50, (rect.width - 4) / 3);
            EditorGUIUtility.labelWidth = 50;
            ToggleLeft(rect, m_ChildForceExpandWidth, EditorGUIUtility.TrTextContent("Width"));
            rect.x += rect.width + 2;
            ToggleLeft(rect, m_ChildForceExpandHeight, EditorGUIUtility.TrTextContent("Height"));
            EditorGUIUtility.labelWidth = 0;

            EditorGUILayout.PropertyField(m_SmoothDuration);

            serializedObject.ApplyModifiedProperties();
        }

        void ToggleLeft(Rect position, SerializedProperty property, GUIContent label)
        {
            bool toggle = property.boolValue;
            EditorGUI.showMixedValue = property.hasMultipleDifferentValues;
            EditorGUI.BeginChangeCheck();
            int oldIndent = EditorGUI.indentLevel;
            EditorGUI.indentLevel = 0;
            toggle = EditorGUI.ToggleLeft(position, label, toggle);
            EditorGUI.indentLevel = oldIndent;
            if (EditorGUI.EndChangeCheck())
            {
                property.boolValue = property.hasMultipleDifferentValues ? true : !property.boolValue;
            }
            EditorGUI.showMixedValue = false;
        }
	}

@bawahakim
Copy link

Another thing: I realized that the smooth transition also happens on initialization, which can look weird in some scenarios. I've added the following fields:

bool m_IsInit;
TweenerCore<Vector2, Vector2, VectorOptions> setAnchorTweener;

Then:

        protected override void Awake() {
	        base.Awake();
	        StartCoroutine(SetPositionInitCoroutine());
        }
        
        // Wait a few frames so we can skip the initial position animation
        IEnumerator SetPositionInitCoroutine() {
	        yield return null;
	        yield return null;
	        m_IsInit = true;
        }

And finally:

        public void SetPosition(RectTransform rect, Vector2 pos, int axis) {
            
            if (!Application.isPlaying) {
                rect.anchoredPosition = pos;
            }

            switch (axis) {
	            case 0:
	                setAnchorTweener = rect.DOAnchorPosX(pos.x, m_SmoothDuration);
                    break;
                case 1:
	                setAnchorTweener = rect.DOAnchorPosY(pos.y, m_SmoothDuration);
                    break;
            }

            if (!m_IsInit) {
	            setAnchorTweener.Complete();
            }
        }

@codorizzi
Copy link
Author

@bawahakim Thanks for that. One of the benefits of sharing code with public!

Recent updates:

  • Added grid layout group
  • Added the editor layout class (as you mentioned)
  • Removed the dependency with my old project (missed that earlier)
  • Added some code to limit the number of tweens being produced

I'll look into the initialization later on.

@theGaffe
Copy link

theGaffe commented Mar 2, 2022

Thanks a lot for this :) wouldn't believe how much I googled around for solutions for an animated UI layout and they were all were loose ideas. I started to do it myself using DOTween, googled a question, and this page came up. Got it all set up and working perfectly in my project, thank you so much!

@MaroLFC
Copy link

MaroLFC commented Aug 2, 2022

Is there is anyway to make this code run in Unity 2019.4? I tried change using UnityEngine.Pool to using UnityEngine.Rendering but it didn't work

@codorizzi
Copy link
Author

codorizzi commented Aug 2, 2022

Is there is anyway to make this code run in Unity 2019.4? I tried change using UnityEngine.Pool to using UnityEngine.Rendering but it didn't work

I don't have 2019 installed, so I can't post an exact fix. Maybe @bawahakim can comment.

It looks like ListPool is the class being used in UnityEngine.Pool, which was a replacement of the UnityEngine.Rendering class of the same name with a similar interface. If you post your error, I might be able to comment further.

@MaroLFC
Copy link

MaroLFC commented Aug 2, 2022

This is the error in unity

SLayoutGroup.cs(404,20): error CS1525: Invalid expression term '='

and those are the errors in visual studio.

  1. 'ListPool<T>' is inaccessible due to its protection level
  2. Argument 2: cannot convert from 'System.Collections.Generic.List<T>' to 'System.Collections.Generic.List<UnityEngine.Component>'
  3. 'Dictionary<RectTransform, Tween>' does not contain a definition for 'TryRemove' and no accessible extension method 'TryRemove' accepting a first argument of type 'Dictionary<RectTransform, Tween>' could be found (are you missing a using directive or an assembly reference?)

@codorizzi
Copy link
Author

codorizzi commented Aug 4, 2022

For ListPool, according to Unity, it's a private utility class in 2019, and made public in 2020+. You can add the ObjectPool directory (below) to your project to resolve the issue:
https://github.com/needle-mirror/com.unity.localization/tree/master/Runtime/Utilities/ObjectPool

I've also updated TryRemove to just Remove, which resolves that issue.

I've tested the above on 2019 LTS and it worked fine.

@MaroLFC
Copy link

MaroLFC commented Aug 4, 2022

Thank you so much @codorizzi this was really helpful.

I added some other settings to control the animation more using animation curves and start delay for the horizontal and vertical layout groups. I would suggest making an option of a staggered effect so that the items have a slight delay between their animations. Here is an example of the effect I'm talking about https://ibb.co/JdC6BCB

PS. I'm not actually a programmer so the edits could be rough around the edges.

SLayoutGroup:

Added Some new variables to better control the animation

        [SerializeField] public float _startDelay = 0f;
        [SerializeField] public bool _unifiedCurve = true;
        [SerializeField] public AnimationCurve _animationCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1));
        [SerializeField] public AnimationCurve _animationCurveX = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1));
        [SerializeField] public AnimationCurve _animationCurveY = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1));

Added easing and delay to the tween

            switch (axis) {
                
                case 0:
                    dict = RectPositionXTweens;
                    tween = rect.DOAnchorPosX(pos.x, moveDuration).SetEase(SetAnimationCurve(_animationCurveX)).SetDelay(_startDelay);
                    break;
                case 1:
                    dict = RectPositionYTweens;
                    tween = rect.DOAnchorPosY(pos.y, moveDuration).SetEase(SetAnimationCurve(_animationCurveY)).SetDelay(_startDelay);
                    break;
                default:
                    return;
            }

Added a function to split the animation curves for X and Y using a bool

        public AnimationCurve SetAnimationCurve(AnimationCurve TargetCurve)
        {
            return _unifiedCurve ? _animationCurve : TargetCurve;
        }


and updated the first part of HorizontalOrVerticalLayoutGroupEditor:

` public class HorizontalOrVerticalLayoutGroupEditor : Editor
{

    SerializedProperty moveDuration;
    SerializedProperty _startDelay;
    SerializedProperty _unifiedCurve;
    SerializedProperty _animationCurve;
    SerializedProperty _animationCurveX;
    SerializedProperty _animationCurveY;
    SerializedProperty m_Padding;
    SerializedProperty m_Spacing;
    SerializedProperty m_ChildAlignment;
    SerializedProperty m_ChildControlWidth;
    SerializedProperty m_ChildControlHeight;
    SerializedProperty m_ChildScaleWidth;
    SerializedProperty m_ChildScaleHeight;
    SerializedProperty m_ChildForceExpandWidth;
    SerializedProperty m_ChildForceExpandHeight;
    SerializedProperty m_ReverseArrangement;

    protected virtual void OnEnable()
    {
        moveDuration = serializedObject.FindProperty("moveDuration");
        _startDelay = serializedObject.FindProperty("_startDelay");
        _unifiedCurve = serializedObject.FindProperty("_unifiedCurve");
        _animationCurve = serializedObject.FindProperty("_animationCurve");
        _animationCurveX = serializedObject.FindProperty("_animationCurveX");
        _animationCurveY = serializedObject.FindProperty("_animationCurveY");
        m_Padding = serializedObject.FindProperty("m_Padding");
        m_Spacing = serializedObject.FindProperty("m_Spacing");
        m_ChildAlignment = serializedObject.FindProperty("m_ChildAlignment");
        m_ChildControlWidth = serializedObject.FindProperty("m_ChildControlWidth");
        m_ChildControlHeight = serializedObject.FindProperty("m_ChildControlHeight");
        m_ChildScaleWidth = serializedObject.FindProperty("m_ChildScaleWidth");
        m_ChildScaleHeight = serializedObject.FindProperty("m_ChildScaleHeight");
        m_ChildForceExpandWidth = serializedObject.FindProperty("m_ChildForceExpandWidth");
        m_ChildForceExpandHeight = serializedObject.FindProperty("m_ChildForceExpandHeight");
        m_ReverseArrangement = serializedObject.FindProperty("m_ReverseArrangement");
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        EditorGUILayout.LabelField("Tween Settings", EditorStyles.boldLabel);

        EditorGUILayout.BeginHorizontal();
        EditorGUIUtility.labelWidth = 100;
        EditorGUILayout.PropertyField(moveDuration, true);
        EditorGUIUtility.labelWidth = 70;
        EditorGUILayout.PropertyField(_startDelay, true);
        EditorGUILayout.EndHorizontal();

        EditorGUILayout.Space();
        EditorGUILayout.LabelField("Animation Easing", EditorStyles.boldLabel);

        EditorGUIUtility.labelWidth = 120;
        EditorGUILayout.PropertyField(_unifiedCurve, new GUIContent("Use Unified Curve?"));

        if (_unifiedCurve.boolValue)
        {
            EditorGUIUtility.labelWidth = 100;
            EditorGUILayout.PropertyField(_animationCurve, new GUIContent("Curve"));
        }
        else
        {
            EditorGUILayout.BeginHorizontal();
            EditorGUIUtility.labelWidth = 20;
            EditorGUILayout.PropertyField(_animationCurveX, new GUIContent("X"));
            EditorGUILayout.PropertyField(_animationCurveY, new GUIContent("Y"));
            EditorGUILayout.EndHorizontal();
        }

        EditorGUIUtility.labelWidth = 0;
        EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);

        EditorGUILayout.PropertyField(m_Padding, true);
        EditorGUILayout.PropertyField(m_Spacing, true);
        EditorGUILayout.PropertyField(m_ChildAlignment, true);
        EditorGUILayout.PropertyField(m_ReverseArrangement, true);`

@restush
Copy link

restush commented Mar 27, 2023

This is awesome

@Snow-Okami
Copy link

This is amazing, thank you so much! And great work by @MaroLFC with the modifications.

@sebasrez
Copy link

Works really well! Only question is how to spawn an object into the right position? It always gets offset.

@RealAllax
Copy link

Works really well! Only question is how to spawn an object into the right position? It always gets offset.

I literally came across this modification today and have the same question. If there is a solution please let me know

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment