From a1f6c4ef9471fa20ad7c96112082b1e2b061a020 Mon Sep 17 00:00:00 2001 From: PopSlime Date: Mon, 17 Feb 2025 17:16:59 +0800 Subject: [PATCH] feat: Generates group views dynamically --- .../Cryville.Common/Unity/UI/RecyclerView.cs | 160 ++++++++++++++++++ .../Unity/UI/RecyclerView.cs.meta | 11 ++ .../UI/EventGroupListView.cs | 23 ++- Assets/Main.unity | 139 +++++++-------- Assets/Prefabs/UI/Event Group.prefab | 17 +- 5 files changed, 260 insertions(+), 90 deletions(-) create mode 100644 Assets/Cryville.Common/Unity/UI/RecyclerView.cs create mode 100644 Assets/Cryville.Common/Unity/UI/RecyclerView.cs.meta diff --git a/Assets/Cryville.Common/Unity/UI/RecyclerView.cs b/Assets/Cryville.Common/Unity/UI/RecyclerView.cs new file mode 100644 index 0000000..3a42726 --- /dev/null +++ b/Assets/Cryville.Common/Unity/UI/RecyclerView.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace Cryville.Common.Unity.UI { + public delegate void LoadItemHandler(int index, GameObject gameObject); + [RequireComponent(typeof(RectTransform))] + public class RecyclerView : UIBehaviour { + [SerializeField] + private GameObject m_itemTemplate; + public GameObject ItemTemplate { + get { return m_itemTemplate; } + set { m_itemTemplate = value; /*OnTemplateUpdate();*/ } + } + public LoadItemHandler LoadItem { private get; set; } + + public enum Axis { + Horizontal = 0, + Vertical = 1, + } + [SerializeField] + private Axis m_direction; + public Axis Direction { + get { return m_direction; } + set { m_direction = value; /*OnFrameUpdate();*/ } + } + + [SerializeField] + RectOffset m_padding; + public RectOffset Padding { + get => m_padding; + set => m_padding = value; + } + + [SerializeField] + float m_spacing; + public float Spacing { + get => m_spacing; + set => m_spacing = value; + } + + [SerializeField] + private int m_itemCount = 3; + public int ItemCount { + get { return m_itemCount; } + set { m_itemCount = value; /*OnRefresh();*/ } + } + + RectTransform _rectTransform; + protected override void Awake() { + _rectTransform = GetComponent(); + } + + const float _placeholderLength = 100; + int _firstIndex, _lastIndex; + readonly Stack _pool = new(); + void Update() { + int axis = (int)m_direction; + int sign = m_direction == 0 ? 1 : -1; + float padding = axis == 0 ? m_padding.left : m_padding.top; + if (_rectTransform.parent is not RectTransform parentTransform) + throw new InvalidOperationException("Parent transform is not RectTransform"); + + Vector2 apos = _rectTransform.anchoredPosition; + float pos = apos[axis] + padding; + float childPos = _firstIndex * _placeholderLength + padding; + float visibleLength = parentTransform.rect.size[axis]; + + //// Add preceding + //while (_firstIndex > 0 && childPos > pos) { + // var child = Rent(); + // child.transform.SetAsFirstSibling(); + // LoadItem(--_firstIndex, child); + // pos += GetLength(axis, child.transform) - _placeholderLength; + // childPos -= _placeholderLength; + //} + + //// Remove preceding + //while (_firstIndex < _lastIndex) { + // var child = transform.GetChild(0); + // float len = GetLength(axis, child.transform); + // if (childPos + len > pos) break; + // Return(child.gameObject); + // _firstIndex++; + // pos += _placeholderLength - len; + // childPos += _placeholderLength; + //} + + //apos[axis] = pos; + //_rectTransform.anchoredPosition = apos; + + // Layout existing + int index = _firstIndex; + float layoutToPos = pos + visibleLength; + for (; index < _lastIndex && childPos < layoutToPos; index++) { + var child = (RectTransform)transform.GetChild(index - _firstIndex); + LayoutChild(axis, sign, ref childPos, child); + } + + // Remove following + for (; _lastIndex > index; --_lastIndex) { + var child = (RectTransform)transform.GetChild(index - _firstIndex); + Return(child.gameObject); + } + + // Add following (and layout) + while (_lastIndex < m_itemCount && childPos < layoutToPos) { + var child = Rent(); + if (child.transform is not RectTransform childTransform) + throw new InvalidOperationException("Child transform is not RectTransform"); + childTransform.SetSiblingIndex(_lastIndex - _firstIndex); + LoadItem(_lastIndex++, child); + LayoutChild(axis, sign, ref childPos, childTransform); + } + + Vector2 gsize = _rectTransform.sizeDelta; + gsize[axis] = childPos + (m_itemCount - _lastIndex) * _placeholderLength + (axis == 0 ? m_padding.horizontal : m_padding.vertical); + _rectTransform.sizeDelta = gsize; + } + static float GetLength(int axis, Transform child) { + if (child is not RectTransform childTransform) + throw new InvalidOperationException("Child transform is not RectTransform"); + return LayoutUtility.GetPreferredSize(childTransform, axis); + } + void LayoutChild(int axis, int sign, ref float childPos, RectTransform childTransform) { + Vector2 cpos = childTransform.anchoredPosition; + cpos[axis] = childPos * sign; + cpos[axis ^ 1] = axis == 1 ? m_padding.left : m_padding.top; + childTransform.anchoredPosition = cpos; + + Vector2 size = childTransform.sizeDelta; + size[axis ^ 1] = _rectTransform.rect.size[axis ^ 1] - (axis == 1 ? m_padding.horizontal : m_padding.vertical); + childTransform.sizeDelta = size; + + childPos += GetLength(axis, childTransform) + m_spacing; + } + + GameObject Rent() { + if (_pool.TryPop(out var ret)) { + ret.SetActive(true); + return ret; + } + return Instantiate(m_itemTemplate, transform, false); + } + void Return(GameObject child) { + child.transform.SetAsLastSibling(); + child.SetActive(false); + _pool.Push(child); + } + + public void InvalidateAll() { + for (; _lastIndex > _firstIndex; --_lastIndex) { + var child = (RectTransform)transform.GetChild(0); + Return(child.gameObject); + } + } + } +} diff --git a/Assets/Cryville.Common/Unity/UI/RecyclerView.cs.meta b/Assets/Cryville.Common/Unity/UI/RecyclerView.cs.meta new file mode 100644 index 0000000..6b61377 --- /dev/null +++ b/Assets/Cryville.Common/Unity/UI/RecyclerView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1257c1f4490f1d64bb8fc52a9abed1ae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Cryville.EEW.Unity/UI/EventGroupListView.cs b/Assets/Cryville.EEW.Unity/UI/EventGroupListView.cs index fcdb709..6b7ea70 100644 --- a/Assets/Cryville.EEW.Unity/UI/EventGroupListView.cs +++ b/Assets/Cryville.EEW.Unity/UI/EventGroupListView.cs @@ -1,11 +1,19 @@ +using Cryville.Common.Unity.UI; using Cryville.EEW.Core; using System.Collections.Generic; using UnityEngine; namespace Cryville.EEW.Unity.UI { + [RequireComponent(typeof(RecyclerView))] class EventGroupListView : MonoBehaviour { - [SerializeField] - EventGroupView m_prefabEventGroupView; + RecyclerView _recyclerView; + void Awake() { + _recyclerView = GetComponent(); + _recyclerView.LoadItem = LoadItem; + } + void LoadItem(int index, GameObject gameObject) { + gameObject.GetComponent().Set(_groups[^(index + 1)]); + } readonly List _groups = new(); public void UpdateGroup(ReportGroup e) { @@ -17,19 +25,16 @@ namespace Cryville.EEW.Unity.UI { } void Add(ReportGroup group) { _groups.Add(group); - var child = Instantiate(m_prefabEventGroupView); - child.Set(group); - child.transform.SetParent(transform, false); - child.transform.SetSiblingIndex(0); + _recyclerView.ItemCount++; + _recyclerView.InvalidateAll(); } void Remove(ReportGroup group) { int index = _groups.LastIndexOf(group); if (index == -1) return; _groups.RemoveAt(index); - var child = transform.GetChild(_groups.Count - index); - child.SetParent(null, false); - Destroy(child.gameObject); + --_recyclerView.ItemCount; + _recyclerView.InvalidateAll(); } } } diff --git a/Assets/Main.unity b/Assets/Main.unity index 9194211..9dd430c 100644 --- a/Assets/Main.unity +++ b/Assets/Main.unity @@ -468,9 +468,8 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 249202787} - - component: {fileID: 249202790} - - component: {fileID: 249202791} - - component: {fileID: 249202792} + - component: {fileID: 249202789} + - component: {fileID: 249202788} m_Layer: 5 m_Name: _content m_TagString: Untagged @@ -493,52 +492,12 @@ RectTransform: m_Father: {fileID: 800505390} m_RootOrder: 0 m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} - m_AnchorMin: {x: 0, y: 0} + m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 1, y: 1} - m_AnchoredPosition: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: -0.0024414062} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0.5, y: 1} ---- !u!114 &249202790 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 249202786} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Padding: - m_Left: 6 - m_Right: 6 - m_Top: 6 - m_Bottom: 6 - m_ChildAlignment: 0 - m_Spacing: 8 - m_ChildForceExpandWidth: 1 - m_ChildForceExpandHeight: 0 - m_ChildControlWidth: 1 - m_ChildControlHeight: 1 - m_ChildScaleWidth: 0 - m_ChildScaleHeight: 1 - m_ReverseArrangement: 0 ---- !u!114 &249202791 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 249202786} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} - m_Name: - m_EditorClassIdentifier: - m_HorizontalFit: 0 - m_VerticalFit: 2 ---- !u!114 &249202792 +--- !u!114 &249202788 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -550,7 +509,27 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2426bd244f51fed429d955beee52e91d, type: 3} m_Name: m_EditorClassIdentifier: - m_prefabEventGroupView: {fileID: 1040273476696300640, guid: 5d21267de716a844c92260bad4d20b0a, type: 3} +--- !u!114 &249202789 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 249202786} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1257c1f4490f1d64bb8fc52a9abed1ae, type: 3} + m_Name: + m_EditorClassIdentifier: + m_itemTemplate: {fileID: 5834406092508179350, guid: 5d21267de716a844c92260bad4d20b0a, type: 3} + m_direction: 1 + m_padding: + m_Left: 6 + m_Right: 6 + m_Top: 6 + m_Bottom: 6 + m_spacing: 6 + m_itemCount: 0 --- !u!1 &303098820 GameObject: m_ObjectHideFlags: 0 @@ -634,7 +613,7 @@ Camera: m_Enabled: 1 serializedVersion: 2 m_ClearFlags: 2 - m_BackGroundColor: {r: 0, g: 0, b: 0, a: 0} + m_BackGroundColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_projectionMatrixMode: 1 m_GateFitMode: 2 m_FOVAxisMode: 0 @@ -947,7 +926,7 @@ MonoBehaviour: m_cameraController: {fileID: 376792590} m_mapElementManager: {fileID: 1602500234} m_ongoingEventList: {fileID: 719162189} - m_historyEventGroupList: {fileID: 249202792} + m_historyEventGroupList: {fileID: 249202788} --- !u!1 &1349222218 GameObject: m_ObjectHideFlags: 0 @@ -959,7 +938,7 @@ GameObject: - component: {fileID: 1349222219} - component: {fileID: 1349222222} - component: {fileID: 1349222221} - - component: {fileID: 1349222220} + - component: {fileID: 1349222223} m_Layer: 5 m_Name: History Panel m_TagString: Untagged @@ -988,36 +967,6 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 1, y: 0.5} ---- !u!114 &1349222220 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1349222218} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3} - m_Name: - m_EditorClassIdentifier: - m_Content: {fileID: 249202787} - m_Horizontal: 0 - m_Vertical: 1 - m_MovementType: 1 - m_Elasticity: 0.1 - m_Inertia: 1 - m_DecelerationRate: 0.135 - m_ScrollSensitivity: 32 - m_Viewport: {fileID: 800505390} - m_HorizontalScrollbar: {fileID: 0} - m_VerticalScrollbar: {fileID: 0} - m_HorizontalScrollbarVisibility: 0 - m_VerticalScrollbarVisibility: 0 - m_HorizontalScrollbarSpacing: 0 - m_VerticalScrollbarSpacing: 0 - m_OnValueChanged: - m_PersistentCalls: - m_Calls: [] --- !u!114 &1349222221 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1056,6 +1005,36 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1349222218} m_CullTransparentMesh: 1 +--- !u!114 &1349222223 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1349222218} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Content: {fileID: 249202787} + m_Horizontal: 0 + m_Vertical: 1 + m_MovementType: 1 + m_Elasticity: 0.1 + m_Inertia: 1 + m_DecelerationRate: 0.135 + m_ScrollSensitivity: 32 + m_Viewport: {fileID: 800505390} + m_HorizontalScrollbar: {fileID: 0} + m_VerticalScrollbar: {fileID: 0} + m_HorizontalScrollbarVisibility: 0 + m_VerticalScrollbarVisibility: 0 + m_HorizontalScrollbarSpacing: 0 + m_VerticalScrollbarSpacing: 0 + m_OnValueChanged: + m_PersistentCalls: + m_Calls: [] --- !u!1 &1412061071 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Prefabs/UI/Event Group.prefab b/Assets/Prefabs/UI/Event Group.prefab index 445a876..1d46d20 100644 --- a/Assets/Prefabs/UI/Event Group.prefab +++ b/Assets/Prefabs/UI/Event Group.prefab @@ -1271,6 +1271,7 @@ GameObject: m_Component: - component: {fileID: 5834406092508179349} - component: {fileID: 5834406092508179348} + - component: {fileID: 3674434324840772959} - component: {fileID: 1040273476696300640} m_Layer: 5 m_Name: Event Group @@ -1300,7 +1301,7 @@ RectTransform: m_AnchorMin: {x: 0, y: 1} m_AnchorMax: {x: 0, y: 1} m_AnchoredPosition: {x: 0, y: 0} - m_SizeDelta: {x: 320, y: 160} + m_SizeDelta: {x: 320, y: 0} m_Pivot: {x: 0, y: 1} --- !u!114 &5834406092508179348 MonoBehaviour: @@ -1328,6 +1329,20 @@ MonoBehaviour: m_ChildScaleWidth: 0 m_ChildScaleHeight: 0 m_ReverseArrangement: 0 +--- !u!114 &3674434324840772959 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5834406092508179350} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: + m_HorizontalFit: 0 + m_VerticalFit: 2 --- !u!114 &1040273476696300640 MonoBehaviour: m_ObjectHideFlags: 0