using System; using UnityEngine; using UnityEngine.UI; namespace Cryville.Common.Unity.UI { /// /// A handler for loading an item. /// /// The zero-based index of the item. /// The game object for the item instantiated from the item template. /// public delegate bool LoadItemHandler(int index, GameObject gameObject); /// /// A scrollable grid that dynamically loads its items. /// public sealed class ScrollableItemGrid : MonoBehaviour { [SerializeField] [Tooltip("The item template.")] private GameObject m_itemTemplate; /// /// The item template. /// public GameObject ItemTemplate { get { return m_itemTemplate; } set { m_itemTemplate = value; OnTemplateUpdate(); } } /// /// The handler for loading an item. /// public LoadItemHandler LoadItem { private get; set; } /// /// Axis. /// public enum Axis { /// /// Horizontal (x) axis. /// Horizontal = 0, /// /// Vertical (y) axis. /// Vertical = 1, } [SerializeField] [Tooltip("The main axis.")] private Axis m_startAxis; /// /// The main axis. /// public Axis StartAxis { get { return m_startAxis; } set { m_startAxis = value; OnFrameUpdate(); } } [SerializeField] [Tooltip("The item count.")] private int m_itemCount = 3; /// /// The item count. /// public int ItemCount { get { return m_itemCount; } set { m_itemCount = value; OnRefresh(); } } [SerializeField] [Tooltip("The item count per line.")] private int m_lineItemCount = 3; /// /// The item count per line. /// public int LineItemCount { get { return m_lineItemCount; } set { m_lineItemCount = value; OnFrameUpdate(); } } [SerializeField] [Tooltip("The length of the cross axis per line.")] private float m_lineHeight = 100; /// /// The length of the cross axis per line. /// public float LineHeight { get { return m_lineHeight; } set { m_lineHeight = value; OnFrameUpdate(); } } private int LineCount { get { return Mathf.CeilToInt((float)m_itemCount / m_lineItemCount); } } private float GroupHeight { get { return m_lineHeight * LineCount; } } private Vector2 VisibleSize { get { Rect parentRect = ((RectTransform)transform.parent).rect; return new Vector2( m_startAxis == Axis.Horizontal ? parentRect.width : parentRect.height, m_startAxis == Axis.Horizontal ? parentRect.height : parentRect.width ); } } /// /// The maximum count of visible lines. /// public int VisibleLines { get { return Mathf.CeilToInt(VisibleSize.y / m_lineHeight) + 1; } } private bool initialized; private GameObject[][] lines; private int[] refl; Vector2 cpos = new Vector2(0, 1); Vector2 pprectsize; #pragma warning disable IDE0051 void Start() { GetComponentInParent().onValueChanged.AddListener(OnScroll); initialized = true; OnFrameUpdate(); } void Update() { Vector2 cprectsize = ((RectTransform)transform.parent).rect.size; if (cprectsize != pprectsize) { pprectsize = cprectsize; OnFrameUpdate(); } } #pragma warning restore IDE0051 private void OnFrameUpdate() { if (!initialized) return; if (lines != null) for (int i = 0; i < lines.Length; i++) if (lines[i] != null) foreach (GameObject c in lines[i]) GameObject.Destroy(c); lines = new GameObject[VisibleLines][]; refl = new int[VisibleLines]; for (int i = 0; i < refl.Length; i++) refl[i] = i; ResetLines(); SetCells(); ResizeGroup(); } private void OnTemplateUpdate() { if (!initialized) return; ResetLines(); SetCells(); } private void ResetLines() { for (int i = 0; i < lines.Length; i++) { if (lines[i] != null) foreach (GameObject c in lines[i]) GameObject.Destroy(c); lines[i] = new GameObject[LineItemCount]; GenerateLine(i, refl[i]); } } void OnScroll(Vector2 scrpos) { cpos = scrpos; int l0 = GetFirstVisibleLine(cpos); int l1 = l0 + VisibleLines - 1; for (int l = l0; l <= l1; l++) { if (Array.IndexOf(refl, l) != -1) continue; int repl = Array.FindIndex(refl, i => i < l0 || i > l1); if (repl == -1) return; LoadLine(repl, l); SetCells(repl); } } void OnRefresh() { if (!initialized) return; int l0 = GetFirstVisibleLine(cpos); int l1 = l0 + VisibleLines - 1; int i = 0; for (int l = l0; l <= l1; i++, l++) { LoadLine(i, l); SetCells(i); } ResizeGroup(); } void ResizeGroup() { ((RectTransform)transform).sizeDelta = m_startAxis == 0 ? new Vector2(0, GroupHeight) : new Vector2(GroupHeight, 0); } int GetFirstVisibleLine(Vector2 scrpos) { float scr = m_startAxis == Axis.Horizontal ? 1 - scrpos.y : scrpos.x; float maxScroll = Mathf.Max(0, GroupHeight - VisibleSize.y); if (maxScroll == 0) return 0; return Mathf.FloorToInt(scr * maxScroll / LineHeight); } void GenerateLine(int index, int line) { for (int j = 0; j < LineItemCount; j++) { var child = GameObject.Instantiate(m_itemTemplate, transform, false); lines[index][j] = child; } LoadLine(index, line); } void LoadLine(int index, int line) { refl[index] = line; for (int j = 0; j < LineItemCount; j++) { var id = line * m_lineItemCount + j; if (id < 0 || id >= m_itemCount || LoadItem == null) lines[index][j].SetActive(false); else lines[index][j].SetActive(LoadItem(id, lines[index][j])); } } private void SetCells() { for (int i = 0; i < VisibleLines; i++) SetCells(i); } private void SetCells(int index) { float itemWidth = VisibleSize[0] / m_lineItemCount; for (int j = 0; j < LineItemCount; j++) { ((RectTransform)lines[index][j].transform).anchoredPosition = m_startAxis == 0 ? new Vector2(j * itemWidth, -refl[index] * m_lineHeight) : new Vector2(refl[index] * m_lineHeight, -j * itemWidth); ((RectTransform)lines[index][j].transform).sizeDelta = m_startAxis == 0 ? new Vector2(itemWidth, m_lineHeight) : new Vector2(m_lineHeight, itemWidth); } } } }