Created
August 13, 2024 11:45
-
-
Save sinedsem/feda423355c6095fff4d60d3581f3a12 to your computer and use it in GitHub Desktop.
Responsive Tabs for Unity UI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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