Skip to content

Instantly share code, notes, and snippets.

@sinedsem
Created August 13, 2024 11:45
Show Gist options
  • Select an option

  • Save sinedsem/feda423355c6095fff4d60d3581f3a12 to your computer and use it in GitHub Desktop.

Select an option

Save sinedsem/feda423355c6095fff4d60d3581f3a12 to your computer and use it in GitHub Desktop.
Responsive Tabs for Unity UI
using System;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace UI.Tabs
{
[RequireComponent(typeof(Canvas))]
public class ResponsiveTab : MonoBehaviour
{
[SerializeField] private Canvas canvas;
[SerializeField] private Image image;
[SerializeField] private Button button;
[SerializeField] private UnityEvent onActivate;
private Action callback;
private void Awake()
{
button.onClick.AddListener(OnClick);
}
private void OnDestroy()
{
button.onClick.AddListener(OnClick);
}
public void Init(int activeSortingOrder, Action callback)
{
this.callback = callback;
canvas.sortingOrder = activeSortingOrder;
}
private void OnClick()
{
callback?.Invoke();
}
public void SetActive(bool active, TabSprites sprites)
{
image.sprite = active ? sprites.Active : sprites.Inactive;
canvas.overrideSorting = active;
button.interactable = !active;
var spriteState = button.spriteState;
spriteState.highlightedSprite = sprites.Highlighted;
button.spriteState = spriteState;
if (active && Application.isPlaying)
onActivate?.Invoke();
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
namespace UI.Tabs
{
[Serializable]
public class TabSprites
{
[field: SerializeField] public Sprite Active { get; private set; }
[field: SerializeField] public Sprite Inactive { get; private set; }
[field: SerializeField] public Sprite Highlighted { get; private set; }
}
[Serializable]
public class Row
{
[field: SerializeField] public HorizontalLayoutGroup LayoutGroup { get; set; }
[field: SerializeField] public RectTransform RectTransform { get; set; }
[field: SerializeField] public GameObject GameObject { get; set; }
}
[ExecuteAlways]
[RequireComponent(typeof(VerticalLayoutGroup))]
[RequireComponent(typeof(RectTransform))]
public class ResponsiveTabs : MonoBehaviour
{
[SerializeField] private ResponsiveTab[] tabs;
[SerializeField] private int activeTab;
[SerializeField] private float wideLayoutMinWidth = 800;
[SerializeField] private float rowHeight = 100;
[SerializeField] private float rowSpacing = -5;
[SerializeField] private int[] wideConfig = Array.Empty<int>();
[SerializeField] private int[] thinConfig = Array.Empty<int>();
[SerializeField] private TabSprites leftSprites;
[SerializeField] private TabSprites middleSprites;
[SerializeField] private TabSprites rightSprites;
[SerializeField] private TabSprites bothSprites;
private bool initialized;
private RectTransform rectTransform;
private VerticalLayoutGroup column;
private readonly List<Row> rows = new();
private void OnEnable()
{
UpdateTabs();
}
private void OnRectTransformDimensionsChange()
{
if (!initialized)
return;
UpdateTabs();
}
private void UpdateTabs()
{
if (!initialized)
Initialize();
CalculateLayout();
}
private void CalculateLayout()
{
var groups = rectTransform.rect.width >= wideLayoutMinWidth ? wideConfig : thinConfig;
while (rows.Count < groups.Length)
rows.Add(MakeRow());
MixGroups(groups, out var mixedTabs, out var mixedGroups, out var activeMixedTab);
var tabCounter = 0;
for (var rowIndex = 0; rowIndex < mixedGroups.Count && rowIndex < rows.Count; rowIndex++)
{
var row = rows[rowIndex];
row.GameObject.SetActive(true);
var height = rowIndex == mixedGroups.Count - 1 ? rowHeight : rowHeight * 2;
row.RectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
// iterate group
for (var i = 0; i < mixedGroups[rowIndex]; i++)
{
if (tabCounter >= mixedTabs.Count)
goto end_of_loop;
var tab = mixedTabs[tabCounter];
tab.transform.SetParent(row.RectTransform, false);
tab.transform.SetSiblingIndex(i);
tab.gameObject.SetActive(true);
tab.SetActive(activeMixedTab == tabCounter, ChooseSprites(mixedGroups[rowIndex], i));
tabCounter++;
}
}
end_of_loop :
{
}
for (var i = groups.Length; i < rows.Count; i++)
rows[i].GameObject.SetActive(false);
for (var i = tabCounter; i < tabs.Length; i++)
tabs[i].gameObject.SetActive(false);
}
private void MixGroups(int[] groups,
out List<ResponsiveTab> mixedTabs, out List<int> mixedGroups, out int activeMixedTab)
{
mixedTabs = new List<ResponsiveTab>();
mixedGroups = new List<int>();
var total = 0;
var activeTabGroup = 0;
var activeTabGroupStart = 0;
var activeTabInGroup = 0;
foreach (var group in groups)
{
if (total + group <= activeTab || activeTabGroup > 0)
{
AddGroupToMixed(mixedTabs, mixedGroups, group, total);
}
else
{
activeTabGroup = group;
activeTabGroupStart = total;
activeTabInGroup = activeTab - total;
}
total += group;
}
activeMixedTab = total - activeTabGroup + activeTabInGroup;
AddGroupToMixed(mixedTabs, mixedGroups, activeTabGroup, activeTabGroupStart);
}
private void AddGroupToMixed(List<ResponsiveTab> mixedTabs, List<int> mixedGroups, int group, int startIndex)
{
mixedGroups.Add(group);
for (var i = 0; i < group; i++)
mixedTabs.Add(tabs[startIndex + i]);
}
private TabSprites ChooseSprites(int groupSize, int i)
{
if (groupSize == 1)
return bothSprites;
if (i == 0)
return leftSprites;
if (i == groupSize - 1)
return rightSprites;
return middleSprites;
}
private void Initialize()
{
rectTransform = GetComponent<RectTransform>();
column = GetComponent<VerticalLayoutGroup>();
column.childControlWidth = true;
column.childControlHeight = false;
column.childScaleWidth = false;
column.childScaleHeight = false;
column.childForceExpandWidth = true;
column.childForceExpandHeight = false;
rows.Clear();
for (var i = 0; i < column.transform.childCount; i++)
{
if (column.transform.GetChild(i).TryGetComponent<HorizontalLayoutGroup>(out var layoutGroup))
{
var row = new Row
{
LayoutGroup = layoutGroup,
RectTransform = layoutGroup.GetComponent<RectTransform>(),
GameObject = layoutGroup.gameObject
};
ApplyRowSettings(row);
rows.Add(row);
}
}
var parentCanvas = rectTransform.GetComponentInParent<Canvas>();
var activeTabSortingOrder = parentCanvas ? parentCanvas.sortingOrder + 1 : 0;
for (var i = 0; i < tabs.Length; i++)
{
var tab = tabs[i];
var finalI = i;
tab.Init(activeTabSortingOrder, () => OnTabClick(finalI));
}
CheckConfig(thinConfig, "thin");
CheckConfig(wideConfig, "wide");
initialized = true;
}
private void OnTabClick(int tabIndex)
{
activeTab = tabIndex;
UpdateTabs();
}
private void CheckConfig(int[] config, string configName)
{
var sum = config.Sum();
if (sum != tabs.Length)
Debug.LogWarning($"Incorrect {configName} config, sum {sum}, tabs {tabs.Length}");
}
private Row MakeRow()
{
var go = new GameObject("Row");
var layoutGroup = go.AddComponent<HorizontalLayoutGroup>();
go.transform.SetParent(column.transform, false);
var row = new Row
{
LayoutGroup = layoutGroup,
RectTransform = go.GetComponent<RectTransform>(),
GameObject = go
};
ApplyRowSettings(row);
return row;
}
private void ApplyRowSettings(Row row)
{
row.LayoutGroup.spacing = rowSpacing;
row.LayoutGroup.childControlWidth = true;
row.LayoutGroup.childControlHeight = true;
row.LayoutGroup.childScaleWidth = false;
row.LayoutGroup.childScaleHeight = false;
row.LayoutGroup.childForceExpandWidth = true;
row.LayoutGroup.childForceExpandHeight = true;
}
#if UNITY_EDITOR
private void Update()
{
if (Application.isPlaying)
return;
UpdateTabs();
}
private void OnValidate()
{
if (Application.isPlaying)
return;
initialized = false;
UpdateTabs();
}
#endif
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment