├── LICENSE ├── README.md ├── VariableGridCell.cs └── VariableGridLayoutGroup.cs /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Robin King 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VariableGridLayoutGroup 2 | 3 | The built-in GridLayoutGroup component in Unity's UI is limited to identical cell sizes specified in the inspector. This custom script allows you to create a grid whose columns and rows are variable sizes, dynamically resizing to fit the largest content in that row or column. 4 | 5 | An explainer video is hosted at: https://www.youtube.com/watch?v=m4a_WFMDB50 6 | 7 | NB: If a cell contains a Text element which is set to wrap, the cell may become taller than needed. To get round this, it is best practice to make every cell a GameObject containing a Horizontal Layout Group, with Child Controls Size true and Child Force Expand false. Then attach a child to this cell object, and add the text element there instead. If desired, you can add a LayoutElement to the cell root object and set a preferred width/height. 8 | 9 | NB: If you add a VariableGridCell element to a cell, you can override Force Expand etc. However, if you disable the VariableGridCell, you may need to disable and enable the GridLayoutGroup to refresh the layout. 10 | -------------------------------------------------------------------------------- /VariableGridCell.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine.EventSystems; 2 | 3 | namespace UnityEngine.UI 4 | { 5 | [AddComponentMenu("Layout/Variable Grid Layout Group Cell", 140)] 6 | [RequireComponent(typeof(RectTransform))] 7 | [ExecuteInEditMode] 8 | public class VariableGridCell : UIBehaviour 9 | { 10 | [SerializeField] 11 | private bool m_OverrideForceExpandWidth = false; 12 | public virtual bool overrideForceExpandWidth { 13 | get { return m_OverrideForceExpandWidth; } 14 | set { 15 | if (value != m_OverrideForceExpandWidth) { 16 | m_OverrideForceExpandWidth = value; 17 | SetDirty (); 18 | } 19 | } 20 | } 21 | 22 | [SerializeField] 23 | private bool m_ForceExpandWidth = false; 24 | public virtual bool forceExpandWidth { 25 | get { return m_ForceExpandWidth; } 26 | set { 27 | if (value != m_ForceExpandWidth) { 28 | m_ForceExpandWidth = value; 29 | SetDirty (); 30 | } 31 | } 32 | } 33 | 34 | [SerializeField] 35 | private bool m_OverrideForceExpandHeight = false; 36 | public virtual bool overrideForceExpandHeight { 37 | get { return m_OverrideForceExpandHeight; } 38 | set { 39 | if (value != m_OverrideForceExpandHeight) { 40 | m_OverrideForceExpandHeight = value; 41 | SetDirty (); 42 | } 43 | } 44 | } 45 | 46 | [SerializeField] 47 | private bool m_ForceExpandHeight = false; 48 | public virtual bool forceExpandHeight { 49 | get { return m_ForceExpandHeight; } 50 | set { 51 | if (value != m_ForceExpandHeight) { 52 | m_ForceExpandHeight = value; 53 | SetDirty (); 54 | } 55 | } 56 | } 57 | 58 | [SerializeField] 59 | private bool m_OverrideCellAlignment = false; 60 | public virtual bool overrideCellAlignment { 61 | get { return m_OverrideCellAlignment; } 62 | set { 63 | if (value != m_OverrideCellAlignment) { 64 | m_OverrideCellAlignment = value; 65 | SetDirty (); 66 | } 67 | } 68 | } 69 | 70 | [SerializeField] 71 | private TextAnchor m_CellAlignment = TextAnchor.UpperLeft; 72 | public virtual TextAnchor cellAlignment { 73 | get { return m_CellAlignment; } 74 | set { 75 | if (value != m_CellAlignment) { 76 | m_CellAlignment = value; 77 | SetDirty (); 78 | } 79 | } 80 | } 81 | 82 | 83 | 84 | protected VariableGridCell() 85 | {} 86 | 87 | #region Unity Lifetime calls 88 | 89 | protected override void OnEnable() 90 | { 91 | base.OnEnable(); 92 | SetDirty(); 93 | } 94 | 95 | protected override void OnTransformParentChanged() 96 | { 97 | SetDirty(); 98 | } 99 | 100 | protected override void OnDisable() 101 | { 102 | SetDirty(); 103 | base.OnDisable(); 104 | } 105 | 106 | protected override void OnDidApplyAnimationProperties() 107 | { 108 | SetDirty(); 109 | } 110 | 111 | protected override void OnBeforeTransformParentChanged() 112 | { 113 | SetDirty(); 114 | } 115 | 116 | #endregion 117 | 118 | protected void SetDirty() 119 | { 120 | if (!IsActive()) 121 | return; 122 | LayoutRebuilder.MarkLayoutForRebuild(transform as RectTransform); 123 | } 124 | 125 | #if UNITY_EDITOR 126 | protected override void OnValidate() 127 | { 128 | SetDirty(); 129 | } 130 | 131 | #endif 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /VariableGridLayoutGroup.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using System.Collections.Generic; 3 | 4 | namespace UnityEngine.UI 5 | { 6 | [AddComponentMenu("Layout/Variable Grid Layout Group", 152)] 7 | public class VariableGridLayoutGroup : LayoutGroup 8 | { 9 | public enum Corner { UpperLeft = 0, UpperRight = 1, LowerLeft = 2, LowerRight = 3 } 10 | public enum Axis { Horizontal = 0, Vertical = 1 } 11 | public enum Constraint { FixedColumnCount = 0, FixedRowCount = 1 } 12 | 13 | [SerializeField] protected Corner m_StartCorner = Corner.UpperLeft; 14 | public Corner startCorner { get { return m_StartCorner; } set { SetProperty(ref m_StartCorner, value); } } 15 | 16 | [SerializeField] protected Axis m_StartAxis = Axis.Horizontal; 17 | public Axis startAxis { get { return m_StartAxis; } set { SetProperty(ref m_StartAxis, value); } } 18 | 19 | [SerializeField] protected TextAnchor m_CellAlignment = TextAnchor.UpperLeft; 20 | public TextAnchor cellAlignment { get { return m_CellAlignment; } set { SetProperty(ref m_CellAlignment, value); } } 21 | 22 | [SerializeField] protected Vector2 m_Spacing = Vector2.zero; 23 | public Vector2 spacing { get { return m_Spacing; } set { SetProperty(ref m_Spacing, value); } } 24 | 25 | [SerializeField] protected Constraint m_Constraint = Constraint.FixedColumnCount; 26 | public Constraint constraint { get { return m_Constraint; } set { SetProperty(ref m_Constraint, value); } } 27 | 28 | [SerializeField] protected int m_ConstraintCount = 3; 29 | public int constraintCount { get { return m_ConstraintCount; } set { SetProperty(ref m_ConstraintCount, Mathf.Max(1, value)); } } 30 | 31 | [SerializeField] protected bool m_ChildForceExpandWidth = true; 32 | public bool childForceExpandWidth { get { return m_ChildForceExpandWidth; } set { SetProperty(ref m_ChildForceExpandWidth, value); } } 33 | 34 | [SerializeField] protected bool m_ChildForceExpandHeight = true; 35 | public bool childForceExpandHeight { get { return m_ChildForceExpandHeight; } set { SetProperty(ref m_ChildForceExpandHeight, value); } } 36 | 37 | protected VariableGridLayoutGroup() 38 | {} 39 | 40 | #if UNITY_EDITOR 41 | protected override void OnValidate() 42 | { 43 | base.OnValidate(); 44 | constraintCount = constraintCount; 45 | } 46 | 47 | #endif 48 | 49 | //------------------------------------------------------------------------------------------------------ 50 | public int columns { get; private set; } 51 | public int rows { get; private set; } 52 | private int[,] cellIndexAtGridRef; 53 | private int[] cellColumn; 54 | private int[] cellRow; 55 | private Vector2[] cellPreferredSizes; 56 | private float[] columnWidths; 57 | private float[] rowHeights; 58 | private float totalColumnWidth; 59 | private float totalRowHeight; 60 | 61 | //------------------------------------------------------------------------------------------------------ 62 | public int GetCellIndexAtGridRef( int column, int row ) { 63 | if (column >= 0 && column < columns && row >= 0 && row < rows) 64 | return cellIndexAtGridRef [column, row]; 65 | else 66 | return -1; 67 | } 68 | 69 | //------------------------------------------------------------------------------------------------------ 70 | public int GetCellColumn( int cellIndex ) { 71 | if (cellIndex >= 0 && cellIndex < rectChildren.Count) 72 | return cellColumn [cellIndex]; 73 | else 74 | return -1; 75 | } 76 | 77 | //------------------------------------------------------------------------------------------------------ 78 | public int GetCellRow( int cellIndex ) { 79 | if (cellIndex >= 0 && cellIndex < rectChildren.Count) 80 | return cellRow [cellIndex]; 81 | else 82 | return -1; 83 | } 84 | 85 | //------------------------------------------------------------------------------------------------------ 86 | public float GetColumnPositionWithinGrid( int column ) { 87 | 88 | if (column <= 0 || column >= columns) 89 | return 0; 90 | 91 | float pos = 0; 92 | for (int c = 0; c < column; c++) { 93 | pos += GetColumnWidth (c) + spacing.x; 94 | } 95 | return pos; 96 | } 97 | 98 | //------------------------------------------------------------------------------------------------------ 99 | public float GetRowPositionWithinGrid( int row ) { 100 | 101 | if (row <= 0 || row >= rows) 102 | return 0; 103 | 104 | float pos = 0; 105 | for (int r = 0; r < row; r++) { 106 | pos += GetRowHeight (r) + spacing.y; 107 | } 108 | return pos; 109 | } 110 | 111 | //------------------------------------------------------------------------------------------------------ 112 | public float GetColumnWidth( int column ) { 113 | 114 | if (column < 0 || column >= columns) 115 | return 0; 116 | 117 | return columnWidths [column]; 118 | } 119 | 120 | //------------------------------------------------------------------------------------------------------ 121 | public float GetRowHeight( int row ) { 122 | 123 | if (row < 0 || row >= rows) 124 | return 0; 125 | 126 | return rowHeights [row]; 127 | } 128 | 129 | //------------------------------------------------------------------------------------------------------ 130 | private void InitializeLayout() { 131 | 132 | columns = (constraint == Constraint.FixedColumnCount) ? Mathf.Min(constraintCount, rectChildren.Count) : Mathf.CeilToInt((float)rectChildren.Count / (float)constraintCount); 133 | rows = (constraint == Constraint.FixedRowCount) ? Mathf.Min(constraintCount, rectChildren.Count) : Mathf.CeilToInt((float)rectChildren.Count / (float)constraintCount); 134 | 135 | cellIndexAtGridRef = new int[columns, rows]; 136 | cellColumn = new int[rectChildren.Count]; 137 | cellRow = new int[rectChildren.Count]; 138 | cellPreferredSizes = new Vector2[rectChildren.Count]; 139 | columnWidths = new float[columns]; 140 | rowHeights = new float[rows]; 141 | totalColumnWidth = 0; 142 | totalRowHeight = 0; 143 | for (int a = 0; a < columns; a++) { 144 | for (int b = 0; b < rows; b++) { 145 | cellIndexAtGridRef [a, b] = -1; 146 | } 147 | } 148 | 149 | int cOrigin = 0; 150 | int rOrigin = 0; 151 | int cNext = 1; 152 | int rNext = 1; 153 | if (startCorner == Corner.UpperRight || startCorner == Corner.LowerRight) { 154 | cOrigin = columns - 1; 155 | cNext = -1; 156 | } 157 | if (startCorner == Corner.LowerLeft || startCorner == Corner.LowerRight) { 158 | rOrigin = rows - 1; 159 | rNext = -1; 160 | } 161 | int c = cOrigin; 162 | int r = rOrigin; 163 | 164 | for (int cell = 0; cell < rectChildren.Count; cell++) { 165 | cellIndexAtGridRef [c, r] = cell; 166 | cellColumn [cell] = c; 167 | cellRow [cell] = r; 168 | cellPreferredSizes [cell] = new Vector2 (LayoutUtility.GetPreferredWidth(rectChildren[cell]), LayoutUtility.GetPreferredHeight(rectChildren[cell])); 169 | columnWidths [c] = Mathf.Max (columnWidths [c], cellPreferredSizes [cell].x); 170 | rowHeights [r] = Mathf.Max (rowHeights [r], cellPreferredSizes [cell].y); 171 | 172 | // next 173 | if (startAxis == Axis.Horizontal) { 174 | c += cNext; 175 | if (c < 0 || c >= columns) { 176 | c = cOrigin; 177 | r += rNext; 178 | } 179 | } else { 180 | r += rNext; 181 | if (r < 0 || r >= rows) { 182 | r = rOrigin; 183 | c += cNext; 184 | } 185 | } 186 | } 187 | 188 | for (int col = 0; col < columns; col++) { 189 | totalColumnWidth += columnWidths[col]; 190 | } 191 | for (int row = 0; row < rows; row++) { 192 | totalRowHeight += rowHeights[row]; 193 | } 194 | } 195 | 196 | 197 | 198 | //------------------------------------------------------------------------------------------------------ 199 | public override void CalculateLayoutInputHorizontal() 200 | { 201 | base.CalculateLayoutInputHorizontal(); 202 | 203 | InitializeLayout (); 204 | 205 | float totalMinWidth = padding.horizontal; 206 | float totalPreferredWidth = padding.horizontal + totalColumnWidth + spacing.x * (columns - 1); 207 | 208 | SetLayoutInputForAxis (totalMinWidth, totalPreferredWidth, -1, 0); 209 | 210 | // Stretch if there is a layout element specifying extra space and child force expand width is on 211 | float extraWidth = LayoutUtility.GetPreferredWidth (rectTransform) - totalPreferredWidth; 212 | if (extraWidth > 0 && childForceExpandWidth) { 213 | 214 | // Don't expand column if all cells in column override expansion to false 215 | bool[] expandColumn = new bool[columns]; 216 | int columnsToExpand = 0; 217 | for (int c = 0; c < columns; c++) { 218 | expandColumn [c] = false; 219 | for (int r = 0; r < rows; r++) { 220 | int index = GetCellIndexAtGridRef (c, r); 221 | if (index < rectChildren.Count) { 222 | var child = rectChildren [index]; 223 | var cellOptions = child.GetComponent (); 224 | if (cellOptions == null || !cellOptions.overrideForceExpandWidth || cellOptions.forceExpandWidth) { 225 | expandColumn [c] = true; 226 | columnsToExpand++; 227 | break; 228 | } 229 | } 230 | } 231 | } 232 | 233 | // Give extra space equally - for future version could also make option to give extra space proportionally 234 | for (int c = 0; c < columns; c++) { 235 | if (expandColumn[c]) 236 | columnWidths [c] += extraWidth / columnsToExpand; 237 | } 238 | } 239 | } 240 | 241 | 242 | 243 | //------------------------------------------------------------------------------------------------------ 244 | public override void CalculateLayoutInputVertical() 245 | { 246 | float totalMinHeight = padding.vertical; 247 | float totalPreferredHeight = padding.vertical + totalRowHeight + spacing.y * (rows - 1); 248 | 249 | SetLayoutInputForAxis (totalMinHeight, totalPreferredHeight, -1, 1); 250 | 251 | // Stretch if there is a layout element specifying extra space and child force expand height is on 252 | float extraHeight = LayoutUtility.GetPreferredHeight (rectTransform) - totalPreferredHeight; 253 | if (extraHeight > 0 && childForceExpandHeight) { 254 | 255 | // Don't expand column if all cells in column override expansion to false 256 | bool[] expandRow = new bool[rows]; 257 | int rowsToExpand = 0; 258 | for (int r = 0; r < rows; r++) { 259 | expandRow [r] = false; 260 | for (int c = 0; c < columns; c++) { 261 | int index = GetCellIndexAtGridRef (c, r); 262 | if (index >= 0 && index < rectChildren.Count) { 263 | var child = rectChildren [index]; 264 | var cellOptions = child.GetComponent (); 265 | if (cellOptions == null || !cellOptions.overrideForceExpandHeight || cellOptions.forceExpandHeight) { 266 | expandRow [r] = true; 267 | rowsToExpand++; 268 | break; 269 | } 270 | } else { 271 | expandRow [r] = true; 272 | rowsToExpand++; 273 | break; 274 | } 275 | } 276 | } 277 | 278 | // Give extra space equally 279 | for (int r = 0; r < rows; r++) { 280 | if (expandRow [r]) { 281 | rowHeights [r] += extraHeight / rowsToExpand; 282 | } 283 | } 284 | } 285 | 286 | } 287 | 288 | //------------------------------------------------------------------------------------------------------ 289 | public override void SetLayoutHorizontal() 290 | { 291 | SetCellsAlongAxis(0); 292 | } 293 | 294 | //------------------------------------------------------------------------------------------------------ 295 | public override void SetLayoutVertical() 296 | { 297 | SetCellsAlongAxis(1); 298 | } 299 | 300 | //------------------------------------------------------------------------------------------------------ 301 | private void SetCellsAlongAxis(int axis) 302 | { 303 | // Get origin 304 | float space = (axis == 0 ? rectTransform.rect.width : rectTransform.rect.height); 305 | float extraSpace = space - LayoutUtility.GetPreferredSize (rectTransform, axis); 306 | 307 | float gridOrigin = (axis == 0 ? padding.left : padding.top); 308 | if (axis == 0) { 309 | if (childAlignment == TextAnchor.UpperCenter || childAlignment == TextAnchor.MiddleCenter || childAlignment == TextAnchor.LowerCenter) { 310 | gridOrigin += extraSpace / 2f; 311 | } 312 | else if (childAlignment == TextAnchor.UpperRight || childAlignment == TextAnchor.MiddleRight || childAlignment == TextAnchor.LowerRight) { 313 | gridOrigin += extraSpace; 314 | } 315 | } else { 316 | if (childAlignment == TextAnchor.MiddleLeft || childAlignment == TextAnchor.MiddleCenter || childAlignment == TextAnchor.MiddleRight) { 317 | gridOrigin += extraSpace / 2f; 318 | } 319 | else if (childAlignment == TextAnchor.LowerLeft || childAlignment == TextAnchor.LowerCenter || childAlignment == TextAnchor.LowerRight) { 320 | gridOrigin += extraSpace; 321 | } 322 | } 323 | 324 | // Expansion/alignment options 325 | bool forceExpand = (axis == 0) ? childForceExpandWidth : childForceExpandHeight; 326 | int alignment = 0; 327 | if (axis == 0) { 328 | if (cellAlignment == TextAnchor.UpperLeft || cellAlignment == TextAnchor.MiddleLeft || cellAlignment == TextAnchor.LowerLeft) 329 | alignment = -1; 330 | if (cellAlignment == TextAnchor.UpperRight || cellAlignment == TextAnchor.MiddleRight || cellAlignment == TextAnchor.LowerRight) 331 | alignment = 1; 332 | } else { 333 | if (cellAlignment == TextAnchor.UpperLeft || cellAlignment == TextAnchor.UpperCenter || cellAlignment == TextAnchor.UpperRight) 334 | alignment = -1; 335 | if (cellAlignment == TextAnchor.LowerLeft || cellAlignment == TextAnchor.LowerCenter || cellAlignment == TextAnchor.LowerRight) 336 | alignment = 1; 337 | } 338 | 339 | // Set cells 340 | for (int i = 0; i < rectChildren.Count; i++) { 341 | 342 | int colrow = (axis == 0 ? GetCellColumn (i) : GetCellRow (i)); 343 | 344 | // Column/row origin 345 | float cellOrigin = gridOrigin + (axis == 0 ? GetColumnPositionWithinGrid(colrow) : GetRowPositionWithinGrid(colrow)); 346 | 347 | // Column/row size and space 348 | float cellSpace = (axis == 0 ? GetColumnWidth(colrow) : GetRowHeight(colrow)); 349 | var child = rectChildren[i]; 350 | float cellSize = LayoutUtility.GetPreferredSize(child,axis); 351 | float cellExtraSpace = cellSpace - cellSize; 352 | 353 | // If cell should stretch, place there. If not, place within cell space according to cell alignment and its preferred size 354 | bool cellForceExpand = forceExpand; 355 | int thisCellAlignment = alignment; 356 | var cellOptions = child.GetComponent (); 357 | if (cellOptions != null) { 358 | if (axis == 0 ? cellOptions.overrideForceExpandWidth : cellOptions.overrideForceExpandHeight) 359 | cellForceExpand = (axis == 0 ? cellOptions.forceExpandWidth : cellOptions.forceExpandHeight); 360 | if (cellOptions.overrideCellAlignment) { 361 | if (axis == 0) { 362 | if (cellOptions.cellAlignment == TextAnchor.UpperLeft || cellOptions.cellAlignment == TextAnchor.MiddleLeft || cellOptions.cellAlignment == TextAnchor.LowerLeft) 363 | thisCellAlignment = -1; 364 | else if (cellOptions.cellAlignment == TextAnchor.UpperCenter || cellOptions.cellAlignment == TextAnchor.MiddleCenter || cellOptions.cellAlignment == TextAnchor.LowerCenter) 365 | thisCellAlignment = 0; 366 | else 367 | thisCellAlignment = 1; 368 | } else { 369 | if (cellOptions.cellAlignment == TextAnchor.UpperLeft || cellOptions.cellAlignment == TextAnchor.UpperCenter || cellOptions.cellAlignment == TextAnchor.UpperRight) 370 | thisCellAlignment = -1; 371 | else if (cellOptions.cellAlignment == TextAnchor.MiddleLeft || cellOptions.cellAlignment == TextAnchor.MiddleCenter || cellOptions.cellAlignment == TextAnchor.MiddleRight) 372 | thisCellAlignment = 0; 373 | else 374 | thisCellAlignment = 1; 375 | } 376 | } 377 | } 378 | if (cellForceExpand) { 379 | cellSize = cellSpace; 380 | } else { 381 | if (thisCellAlignment == 0) 382 | cellOrigin += cellExtraSpace / 2f; 383 | if (thisCellAlignment == 1) 384 | cellOrigin += cellExtraSpace; 385 | } 386 | 387 | SetChildAlongAxis (rectChildren [i], axis, cellOrigin, cellSize); 388 | } 389 | } 390 | } 391 | } 392 | --------------------------------------------------------------------------------