From 9d941a179d9ef5d997d700b4f7eb4afd792e2d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Wed, 12 Mar 2025 20:59:12 +0800 Subject: [PATCH] modify --- Runtime/AlicizaX.UI.Extension.asmdef | 3 +- Runtime/RecyclerView.meta | 8 + Runtime/RecyclerView/Adapter.meta | 8 + Runtime/RecyclerView/Adapter/Adapter.cs | 198 ++++++++++ Runtime/RecyclerView/Adapter/Adapter.cs.meta | 11 + Runtime/RecyclerView/Adapter/GroupAdapter.cs | 128 ++++++ .../RecyclerView/Adapter/GroupAdapter.cs.meta | 11 + Runtime/RecyclerView/Adapter/IAdapter.cs | 15 + Runtime/RecyclerView/Adapter/IAdapter.cs.meta | 11 + Runtime/RecyclerView/Adapter/LoopAdapter.cs | 36 ++ .../RecyclerView/Adapter/LoopAdapter.cs.meta | 11 + Runtime/RecyclerView/Adapter/MixedAdapter.cs | 43 ++ .../RecyclerView/Adapter/MixedAdapter.cs.meta | 11 + Runtime/RecyclerView/EaseUtil.cs | 192 +++++++++ Runtime/RecyclerView/EaseUtil.cs.meta | 3 + Runtime/RecyclerView/Layout.meta | 8 + .../Layout/CircleLayoutManager.cs | 121 ++++++ .../Layout/CircleLayoutManager.cs.meta | 11 + .../RecyclerView/Layout/GridLayoutManager.cs | 124 ++++++ .../Layout/GridLayoutManager.cs.meta | 11 + Runtime/RecyclerView/Layout/ILayoutManager.cs | 115 ++++++ .../Layout/ILayoutManager.cs.meta | 11 + Runtime/RecyclerView/Layout/LayoutManager.cs | 230 +++++++++++ .../RecyclerView/Layout/LayoutManager.cs.meta | 11 + .../Layout/LinearLayoutManager.cs | 102 +++++ .../Layout/LinearLayoutManager.cs.meta | 11 + .../RecyclerView/Layout/MixedLayoutManager.cs | 136 +++++++ .../Layout/MixedLayoutManager.cs.meta | 11 + .../RecyclerView/Layout/PageLayoutManager.cs | 87 +++++ .../Layout/PageLayoutManager.cs.meta | 11 + Runtime/RecyclerView/ObjectPool.meta | 8 + .../ObjectPool/IMixedObjectFactory.cs | 11 + .../ObjectPool/IMixedObjectFactory.cs.meta | 11 + .../ObjectPool/IMixedObjectPool.cs | 8 + .../ObjectPool/IMixedObjectPool.cs.meta | 11 + .../RecyclerView/ObjectPool/IObjectFactory.cs | 28 ++ .../ObjectPool/IObjectFactory.cs.meta | 11 + .../RecyclerView/ObjectPool/IObjectPool.cs | 23 ++ .../ObjectPool/IObjectPool.cs.meta | 11 + .../RecyclerView/ObjectPool/IPooledObject.cs | 5 + .../ObjectPool/IPooledObject.cs.meta | 11 + .../ObjectPool/MixedObjectPool.cs | 101 +++++ .../ObjectPool/MixedObjectPool.cs.meta | 11 + Runtime/RecyclerView/ObjectPool/ObjectPool.cs | 108 +++++ .../ObjectPool/ObjectPool.cs.meta | 11 + .../ObjectPool/UnityComponentFactory.cs | 36 ++ .../ObjectPool/UnityComponentFactory.cs.meta | 11 + .../ObjectPool/UnityGameObjectFactory.cs | 35 ++ .../ObjectPool/UnityGameObjectFactory.cs.meta | 11 + .../ObjectPool/UnityMixedComponentFactory.cs | 61 +++ .../UnityMixedComponentFactory.cs.meta | 11 + .../ObjectPool/UnityMixedGameObjectFactory.cs | 40 ++ .../UnityMixedGameObjectFactory.cs.meta | 11 + Runtime/RecyclerView/RecyclerView.cs | 369 ++++++++++++++++++ Runtime/RecyclerView/RecyclerView.cs.meta | 11 + Runtime/RecyclerView/Scroller.meta | 8 + .../RecyclerView/Scroller/CircleScroller.cs | 61 +++ .../Scroller/CircleScroller.cs.meta | 11 + Runtime/RecyclerView/Scroller/IScroller.cs | 15 + .../RecyclerView/Scroller/IScroller.cs.meta | 11 + Runtime/RecyclerView/Scroller/ScrollbarEx.cs | 80 ++++ .../RecyclerView/Scroller/ScrollbarEx.cs.meta | 11 + Runtime/RecyclerView/Scroller/Scroller.cs | 245 ++++++++++++ .../RecyclerView/Scroller/Scroller.cs.meta | 11 + Runtime/RecyclerView/ViewHolder.meta | 8 + Runtime/RecyclerView/ViewHolder/Example.meta | 8 + .../ViewHolder/Example/SimpleViewHolder.cs | 19 + .../Example/SimpleViewHolder.cs.meta | 11 + Runtime/RecyclerView/ViewHolder/ViewHolder.cs | 59 +++ .../ViewHolder/ViewHolder.cs.meta | 11 + Runtime/RecyclerView/ViewProvider.meta | 8 + .../ViewProvider/MixedViewProvider.cs | 63 +++ .../ViewProvider/MixedViewProvider.cs.meta | 11 + .../ViewProvider/SimpleViewProvider.cs | 51 +++ .../ViewProvider/SimpleViewProvider.cs.meta | 11 + .../RecyclerView/ViewProvider/ViewProvider.cs | 131 +++++++ .../ViewProvider/ViewProvider.cs.meta | 11 + 77 files changed, 3508 insertions(+), 1 deletion(-) create mode 100644 Runtime/RecyclerView.meta create mode 100644 Runtime/RecyclerView/Adapter.meta create mode 100644 Runtime/RecyclerView/Adapter/Adapter.cs create mode 100644 Runtime/RecyclerView/Adapter/Adapter.cs.meta create mode 100644 Runtime/RecyclerView/Adapter/GroupAdapter.cs create mode 100644 Runtime/RecyclerView/Adapter/GroupAdapter.cs.meta create mode 100644 Runtime/RecyclerView/Adapter/IAdapter.cs create mode 100644 Runtime/RecyclerView/Adapter/IAdapter.cs.meta create mode 100644 Runtime/RecyclerView/Adapter/LoopAdapter.cs create mode 100644 Runtime/RecyclerView/Adapter/LoopAdapter.cs.meta create mode 100644 Runtime/RecyclerView/Adapter/MixedAdapter.cs create mode 100644 Runtime/RecyclerView/Adapter/MixedAdapter.cs.meta create mode 100644 Runtime/RecyclerView/EaseUtil.cs create mode 100644 Runtime/RecyclerView/EaseUtil.cs.meta create mode 100644 Runtime/RecyclerView/Layout.meta create mode 100644 Runtime/RecyclerView/Layout/CircleLayoutManager.cs create mode 100644 Runtime/RecyclerView/Layout/CircleLayoutManager.cs.meta create mode 100644 Runtime/RecyclerView/Layout/GridLayoutManager.cs create mode 100644 Runtime/RecyclerView/Layout/GridLayoutManager.cs.meta create mode 100644 Runtime/RecyclerView/Layout/ILayoutManager.cs create mode 100644 Runtime/RecyclerView/Layout/ILayoutManager.cs.meta create mode 100644 Runtime/RecyclerView/Layout/LayoutManager.cs create mode 100644 Runtime/RecyclerView/Layout/LayoutManager.cs.meta create mode 100644 Runtime/RecyclerView/Layout/LinearLayoutManager.cs create mode 100644 Runtime/RecyclerView/Layout/LinearLayoutManager.cs.meta create mode 100644 Runtime/RecyclerView/Layout/MixedLayoutManager.cs create mode 100644 Runtime/RecyclerView/Layout/MixedLayoutManager.cs.meta create mode 100644 Runtime/RecyclerView/Layout/PageLayoutManager.cs create mode 100644 Runtime/RecyclerView/Layout/PageLayoutManager.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool.meta create mode 100644 Runtime/RecyclerView/ObjectPool/IMixedObjectFactory.cs create mode 100644 Runtime/RecyclerView/ObjectPool/IMixedObjectFactory.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/IMixedObjectPool.cs create mode 100644 Runtime/RecyclerView/ObjectPool/IMixedObjectPool.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/IObjectFactory.cs create mode 100644 Runtime/RecyclerView/ObjectPool/IObjectFactory.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/IObjectPool.cs create mode 100644 Runtime/RecyclerView/ObjectPool/IObjectPool.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/IPooledObject.cs create mode 100644 Runtime/RecyclerView/ObjectPool/IPooledObject.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/MixedObjectPool.cs create mode 100644 Runtime/RecyclerView/ObjectPool/MixedObjectPool.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/ObjectPool.cs create mode 100644 Runtime/RecyclerView/ObjectPool/ObjectPool.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/UnityComponentFactory.cs create mode 100644 Runtime/RecyclerView/ObjectPool/UnityComponentFactory.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/UnityGameObjectFactory.cs create mode 100644 Runtime/RecyclerView/ObjectPool/UnityGameObjectFactory.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/UnityMixedComponentFactory.cs create mode 100644 Runtime/RecyclerView/ObjectPool/UnityMixedComponentFactory.cs.meta create mode 100644 Runtime/RecyclerView/ObjectPool/UnityMixedGameObjectFactory.cs create mode 100644 Runtime/RecyclerView/ObjectPool/UnityMixedGameObjectFactory.cs.meta create mode 100644 Runtime/RecyclerView/RecyclerView.cs create mode 100644 Runtime/RecyclerView/RecyclerView.cs.meta create mode 100644 Runtime/RecyclerView/Scroller.meta create mode 100644 Runtime/RecyclerView/Scroller/CircleScroller.cs create mode 100644 Runtime/RecyclerView/Scroller/CircleScroller.cs.meta create mode 100644 Runtime/RecyclerView/Scroller/IScroller.cs create mode 100644 Runtime/RecyclerView/Scroller/IScroller.cs.meta create mode 100644 Runtime/RecyclerView/Scroller/ScrollbarEx.cs create mode 100644 Runtime/RecyclerView/Scroller/ScrollbarEx.cs.meta create mode 100644 Runtime/RecyclerView/Scroller/Scroller.cs create mode 100644 Runtime/RecyclerView/Scroller/Scroller.cs.meta create mode 100644 Runtime/RecyclerView/ViewHolder.meta create mode 100644 Runtime/RecyclerView/ViewHolder/Example.meta create mode 100644 Runtime/RecyclerView/ViewHolder/Example/SimpleViewHolder.cs create mode 100644 Runtime/RecyclerView/ViewHolder/Example/SimpleViewHolder.cs.meta create mode 100644 Runtime/RecyclerView/ViewHolder/ViewHolder.cs create mode 100644 Runtime/RecyclerView/ViewHolder/ViewHolder.cs.meta create mode 100644 Runtime/RecyclerView/ViewProvider.meta create mode 100644 Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs create mode 100644 Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs.meta create mode 100644 Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs create mode 100644 Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs.meta create mode 100644 Runtime/RecyclerView/ViewProvider/ViewProvider.cs create mode 100644 Runtime/RecyclerView/ViewProvider/ViewProvider.cs.meta diff --git a/Runtime/AlicizaX.UI.Extension.asmdef b/Runtime/AlicizaX.UI.Extension.asmdef index e6c19da..2ecf1a6 100644 --- a/Runtime/AlicizaX.UI.Extension.asmdef +++ b/Runtime/AlicizaX.UI.Extension.asmdef @@ -6,7 +6,8 @@ "GUID:5553d74549d54e74cb548b3ab58a8483", "GUID:75b6f2078d190f14dbda4a5b747d709c", "GUID:a19b414bea3b97240a91aeab9a8eab36", - "GUID:198eb6af143bbc4488e2779d96697e06" + "GUID:198eb6af143bbc4488e2779d96697e06", + "GUID:80ecb87cae9c44d19824e70ea7229748" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/RecyclerView.meta b/Runtime/RecyclerView.meta new file mode 100644 index 0000000..45350a2 --- /dev/null +++ b/Runtime/RecyclerView.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 20aaf5daa7c6f1144be40271786f5aca +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Adapter.meta b/Runtime/RecyclerView/Adapter.meta new file mode 100644 index 0000000..c2805af --- /dev/null +++ b/Runtime/RecyclerView/Adapter.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c55a613d7aa2a27448539712f0cf3875 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Adapter/Adapter.cs b/Runtime/RecyclerView/Adapter/Adapter.cs new file mode 100644 index 0000000..019fbe9 --- /dev/null +++ b/Runtime/RecyclerView/Adapter/Adapter.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; + +namespace AlicizaX.UI.RecyclerView +{ + public class Adapter : IAdapter + { + protected RecyclerView recyclerView; + + protected List list; + protected Action onItemClick; + + protected int choiceIndex = -1; + public int ChoiceIndex + { + get => choiceIndex; + set + { + SetChoiceIndex(value); + } + } + + public Adapter(RecyclerView recyclerView) : this(recyclerView, new List(), null) + { + } + + public Adapter(RecyclerView recyclerView, List list) : this(recyclerView, list, null) + { + } + + public Adapter(RecyclerView recyclerView, List list, Action onItemClick) + { + this.recyclerView = recyclerView; + this.list = list; + this.onItemClick = onItemClick; + + this.recyclerView.Adapter = this; + } + + public virtual int GetItemCount() + { + return list == null ? 0 : list.Count; + } + + public virtual int GetRealCount() + { + return GetItemCount(); + } + + public virtual string GetViewName(int index) + { + return ""; + } + + public virtual void OnBindViewHolder(ViewHolder viewHolder, int index) + { + if (index < 0 || index >= GetItemCount()) return; + + T data = list[index]; + + viewHolder.BindViewData(data); + viewHolder.BindItemClick(data, t => + { + SetChoiceIndex(index); + onItemClick?.Invoke(data); + }); + viewHolder.BindChoiceState(index == choiceIndex); + } + + public virtual void NotifyDataChanged() + { + recyclerView.RequestLayout(); + recyclerView.Refresh(); + } + + public virtual void SetList(List list) + { + this.list = list; + recyclerView.Reset(); + NotifyDataChanged(); + } + + public T GetData(int index) + { + if (index < 0 || index >= GetItemCount()) return default; + + return list[index]; + } + + public void Add(T item) + { + list.Add(item); + NotifyDataChanged(); + } + + public void AddRange(IEnumerable collection) + { + list.AddRange(collection); + NotifyDataChanged(); + } + + public void Insert(int index, T item) + { + list.Insert(index, item); + NotifyDataChanged(); + } + + public void InsertRange(int index, IEnumerable collection) + { + list.InsertRange(index, collection); + NotifyDataChanged(); + } + + public void Remove(T item) + { + int index = list.IndexOf(item); + RemoveAt(index); + } + + public void RemoveAt(int index) + { + if (index < 0 || index >= GetItemCount()) return; + + list.RemoveAt(index); + NotifyDataChanged(); + } + + public void RemoveRange(int index, int count) + { + list.RemoveRange(index, count); + NotifyDataChanged(); + } + + public void RemoveAll(Predicate match) + { + list.RemoveAll(match); + NotifyDataChanged(); + } + + public void Clear() + { + list.Clear(); + NotifyDataChanged(); + } + + public void Reverse(int index, int count) + { + list.Reverse(index, count); + NotifyDataChanged(); + } + + public void Reverse() + { + list.Reverse(); + NotifyDataChanged(); + } + + public void Sort(Comparison comparison) + { + list.Sort(comparison); + NotifyDataChanged(); + } + + public void SetOnItemClick(Action onItemClick) + { + this.onItemClick = onItemClick; + } + + protected void SetChoiceIndex(int index) + { + if (index == choiceIndex) return; + + if (choiceIndex != -1) + { + if (TryGetViewHolder(choiceIndex, out var viewHolder)) + { + viewHolder.BindChoiceState(false); + } + } + + choiceIndex = index; + + if (choiceIndex != -1) + { + if (TryGetViewHolder(choiceIndex, out var viewHolder)) + { + viewHolder.BindChoiceState(true); + } + } + } + + private bool TryGetViewHolder(int index, out ViewHolder viewHolder) + { + viewHolder = recyclerView.ViewProvider.GetViewHolder(index); + return viewHolder != null; + } + } +} diff --git a/Runtime/RecyclerView/Adapter/Adapter.cs.meta b/Runtime/RecyclerView/Adapter/Adapter.cs.meta new file mode 100644 index 0000000..a5997c0 --- /dev/null +++ b/Runtime/RecyclerView/Adapter/Adapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f2fe0ab8191b4944db29445f6f796a50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Adapter/GroupAdapter.cs b/Runtime/RecyclerView/Adapter/GroupAdapter.cs new file mode 100644 index 0000000..585ca9e --- /dev/null +++ b/Runtime/RecyclerView/Adapter/GroupAdapter.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; + +namespace AlicizaX.UI.RecyclerView +{ + public class GroupAdapter : Adapter + { + private readonly List showList = new(); + private string groupViewName; + + public GroupAdapter(RecyclerView recyclerView, string groupViewName) : base(recyclerView) + { + this.groupViewName = groupViewName; + } + + public GroupAdapter(RecyclerView recyclerView, List list) : base(recyclerView, list) + { + } + + public GroupAdapter(RecyclerView recyclerView, List list, Action onItemClick) : base(recyclerView, list, onItemClick) + { + } + + public override int GetItemCount() + { + return showList.Count; + } + + public override string GetViewName(int index) + { + return showList[index].viewName; + } + + public override void OnBindViewHolder(ViewHolder viewHolder, int index) + { + if (index < 0 || index >= GetItemCount()) return; + + GroupData data = showList[index]; + + viewHolder.BindViewData(data); + viewHolder.BindItemClick(data, t => + { + if (data.viewName == groupViewName) + { + data.bExpand = !data.bExpand; + NotifyDataChanged(); + } + else + { + SetChoiceIndex(index); + onItemClick?.Invoke(data); + } + }); + } + + public override void NotifyDataChanged() + { + foreach (var data in list) + { + CreateGroup(data.type); + } + + var groupList = showList.FindAll(data => data.viewName == groupViewName); + for (int i = 0; i < groupList.Count; i++) + { + int index = showList.IndexOf(groupList[i]); + Collapse(index); + if (groupList[i].bExpand) + { + Expand(index); + } + } + + foreach (var group in groupList) + { + if (list.FindAll(data => data.type == group.type).Count == 0) + { + showList.Remove(group); + } + } + + base.NotifyDataChanged(); + } + + public override void SetList(List list) + { + showList.Clear(); + base.SetList(list); + } + + private void CreateGroup(int type) + { + var groupData = showList.Find(data => data.type == type && data.viewName == groupViewName); + if (groupData == null) + { + groupData = new GroupData(type, groupViewName, type.ToString()); + showList.Add(groupData); + } + } + + public void Expand(int index) + { + var expandList = list.FindAll(data => data.type == showList[index].type); + showList.InsertRange(index + 1, expandList); + } + + public void Collapse(int index) + { + var collapseList = showList.FindAll(data => data.type == showList[index].type && data.viewName != groupViewName); + showList.RemoveRange(index + 1, collapseList.Count); + } + } + + public class GroupData + { + public bool bExpand; + public int type; + public string viewName; + public string name; + + public GroupData(int type, string viewName, string name) + { + this.type = type; + this.viewName = viewName; + this.name = name; + } + } +} diff --git a/Runtime/RecyclerView/Adapter/GroupAdapter.cs.meta b/Runtime/RecyclerView/Adapter/GroupAdapter.cs.meta new file mode 100644 index 0000000..071409b --- /dev/null +++ b/Runtime/RecyclerView/Adapter/GroupAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1afc560670c303d42b28cc143ed25a8f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Adapter/IAdapter.cs b/Runtime/RecyclerView/Adapter/IAdapter.cs new file mode 100644 index 0000000..a0648ae --- /dev/null +++ b/Runtime/RecyclerView/Adapter/IAdapter.cs @@ -0,0 +1,15 @@ +namespace AlicizaX.UI.RecyclerView +{ + public interface IAdapter + { + int GetItemCount(); + + int GetRealCount(); + + string GetViewName(int index); + + void OnBindViewHolder(ViewHolder viewHolder, int index); + + void NotifyDataChanged(); + } +} diff --git a/Runtime/RecyclerView/Adapter/IAdapter.cs.meta b/Runtime/RecyclerView/Adapter/IAdapter.cs.meta new file mode 100644 index 0000000..539af3e --- /dev/null +++ b/Runtime/RecyclerView/Adapter/IAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fc979439548585f4097747c34ed12270 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Adapter/LoopAdapter.cs b/Runtime/RecyclerView/Adapter/LoopAdapter.cs new file mode 100644 index 0000000..6a7ae6a --- /dev/null +++ b/Runtime/RecyclerView/Adapter/LoopAdapter.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace AlicizaX.UI.RecyclerView +{ + public class LoopAdapter : Adapter + { + public LoopAdapter(RecyclerView recyclerView) : base(recyclerView) + { + } + + public LoopAdapter(RecyclerView recyclerView, List list) : base(recyclerView, list) + { + } + + public LoopAdapter(RecyclerView recyclerView, List list, Action onItemClick) : base(recyclerView, list, onItemClick) + { + } + + public override int GetItemCount() + { + return int.MaxValue; + } + + public override int GetRealCount() + { + return list == null ? 0 : list.Count; + } + + public override void OnBindViewHolder(ViewHolder viewHolder, int index) + { + index %= list.Count; + base.OnBindViewHolder(viewHolder, index); + } + } +} diff --git a/Runtime/RecyclerView/Adapter/LoopAdapter.cs.meta b/Runtime/RecyclerView/Adapter/LoopAdapter.cs.meta new file mode 100644 index 0000000..a74969d --- /dev/null +++ b/Runtime/RecyclerView/Adapter/LoopAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 852ebd0fa448a374aad55404886bfaae +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Adapter/MixedAdapter.cs b/Runtime/RecyclerView/Adapter/MixedAdapter.cs new file mode 100644 index 0000000..d5658ec --- /dev/null +++ b/Runtime/RecyclerView/Adapter/MixedAdapter.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace AlicizaX.UI.RecyclerView +{ + public class MixedAdapter : Adapter + { + public MixedAdapter(RecyclerView recyclerView) : base(recyclerView) + { + } + + public MixedAdapter(RecyclerView recyclerView, List list) : base(recyclerView, list) + { + } + + public MixedAdapter(RecyclerView recyclerView, List list, Action onItemClick) : base(recyclerView, list, onItemClick) + { + } + + public override string GetViewName(int index) + { + return list[index].viewName; + } + } + + public class MixedData + { + public string viewName; + public string name; + public string icon; + public int number; + public int percent; + + public MixedData(string viewName, string name, string icon, int number, int percent) + { + this.viewName = viewName; + this.name = name; + this.icon = icon; + this.number = number; + this.percent = percent; + } + } +} diff --git a/Runtime/RecyclerView/Adapter/MixedAdapter.cs.meta b/Runtime/RecyclerView/Adapter/MixedAdapter.cs.meta new file mode 100644 index 0000000..c893e7c --- /dev/null +++ b/Runtime/RecyclerView/Adapter/MixedAdapter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c335290acc1c8b45aa87e6fc2b00b0c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/EaseUtil.cs b/Runtime/RecyclerView/EaseUtil.cs new file mode 100644 index 0000000..7ac6fe0 --- /dev/null +++ b/Runtime/RecyclerView/EaseUtil.cs @@ -0,0 +1,192 @@ +using System; + +public class EaseUtil +{ + public static double EaseInSine(float x) + { + return 1 - Math.Cos(x * Math.PI / 2); + } + + public static double EaseOutSine(float x) + { + return Math.Sin(x * Math.PI / 2); + } + + public static double EaseInOutSine(float x) + { + return -(Math.Cos(Math.PI * x) - 1) / 2; + } + + public static double EaseInQuad(float x) + { + return x * x; + } + + public static double EaseOutQuad(float x) + { + return 1 - (1 - x) * (1 - x); + } + + public static double EaseInOutQuad(float x) + { + return x < 0.5 ? 2 * x * x : 1 - Math.Pow(-2 * x + 2, 2) / 2; + } + + public static double EaseInQuart(float x) + { + return Math.Pow(x, 4); + } + + public static double EaseOutQuart(float x) + { + return 1 - Math.Pow(1 - x, 4); + } + + public static double EaseInOutQuart(float x) + { + return x < 0.5 ? 8 * x * x * x * x : 1 - Math.Pow(-2 * x + 2, 4) / 2; + } + + public static double EaseInCubic(float x) + { + return Math.Pow(x, 3); + } + + public static double EaseOutCubic(float x) + { + return 1 - Math.Pow(1 - x, 3); + } + + public static double EaseInOutCubic(float x) + { + return x < 0.5f ? 4 * x * x * x : 1 - Math.Pow(-2 * x + 2, 3) / 2; + } + + public static double EaseInQuint(float x) + { + return Math.Pow(x, 5); + } + + public static double EaseOutQuint(float x) + { + return 1 - Math.Pow(1 - x, 5); + } + + public static double EaseInOutQuint(float x) + { + return x < 0.5f ? 16 * Math.Pow(x, 5) : 1 - Math.Pow(-2 * x + 2, 5) / 2; + } + + public static double EaseInExpo(float x) + { + return x == 0 ? 0 : Math.Pow(2, 10 * x - 10); + } + + public static double EaseOutExpo(float x) + { + return x == 1 ? 1 : 1 - Math.Pow(2, -10 * x); + } + + public static double EaseInOutExpo(float x) + { + return x == 0 ? 0 : + x == 1 ? 1 : + x < 0.5 ? Math.Pow(2, 20 * x - 10) / 2 : + (2 - Math.Pow(2, -20 * x + 10)) / 2; + } + + public static double EaseInCirc(float x) + { + return 1 - Math.Sqrt(1 - Math.Pow(x, 2)); + } + + public static double EaseOutCirc(float x) + { + return Math.Sqrt(1 - Math.Pow(x - 1, 2)); + } + + public static double EaseInOutCirc(float x) + { + return x < 0.5 ? (1 - Math.Sqrt(1 - Math.Pow(2 * x, 2))) / 2 : (Math.Sqrt(1 - Math.Pow(-2 * x + 2, 2)) + 1) / 2; + } + + public static double EaseInBack(float x) + { + double c1 = 1.70158; + double c3 = c1 + 1; + + return c3 * x * x * x - c1 * x * x; + } + + public static double EaseOutBack(float x) + { + double c1 = 1.70158; + double c3 = c1 + 1; + + return 1 + c3 * Math.Pow(x - 1, 3) + c1 * Math.Pow(x - 1, 2); + } + + public static double EaseInOutBack(float x) + { + double c1 = 1.70158; + double c2 = c1 * 1.525; + + return x < 0.5 ? + Math.Pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2) / 2 : + (Math.Pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; + } + + public static double EaseInElastic(float x) + { + double c4 = 2 * Math.PI / 3; + return x == 0 ? 0 : x == 1 ? 1 : -Math.Pow(2, 10 * x - 10) * Math.Sin((x * 10 - 10.75) * c4); + } + + public static double EaseOutElastic(float x) + { + double c4 = 2 * Math.PI / 3; + return x == 0 ? 0 : x == 1 ? 1 : Math.Pow(2, -10 * x) * Math.Sin((x * 10 - 0.75) * c4) + 1; + } + + public static double EaseInOutElastic(float x) + { + double c5 = 2 * Math.PI / 4.5; + return x == 0 ? 0 : + x == 1 ? 1 : + x < 0.5 ? -(Math.Pow(2, 20 * x - 10) * Math.Sin((20 * x - 11.125) * c5)) / 2 : + Math.Pow(2, -20 * x + 10) * Math.Sin((20 * x - 11.125) * c5) / 2 + 1; + } + + public static double EaseInBounce(float x) + { + return 1 - EaseOutBounce(1 - x); + } + + public static double EaseOutBounce(float x) + { + double n1 = 7.5625; + double d1 = 2.75; + + if (x < 1 / d1) + { + return n1 * x * x; + } + else if (x < 2 / d1) + { + return n1 * (x -= (float)(1.5 / d1)) * x + 0.75; + } + else if (x < 2.5 / d1) + { + return n1 * (x -= (float)(2.25 / d1)) * x + 0.9375; + } + else + { + return n1 * (x -= (float)(2.625 / d1)) * x + 0.984375; + } + } + + public static double EaseInOutBounce(float x) + { + return x < 0.5 ? (1 - EaseOutBounce(1 - 2 * x)) / 2 : (1 + EaseOutBounce(2 * x - 1)) / 2; + } +} diff --git a/Runtime/RecyclerView/EaseUtil.cs.meta b/Runtime/RecyclerView/EaseUtil.cs.meta new file mode 100644 index 0000000..3b13f5e --- /dev/null +++ b/Runtime/RecyclerView/EaseUtil.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d42e2db77425447490cb9f68003e818b +timeCreated: 1741771999 \ No newline at end of file diff --git a/Runtime/RecyclerView/Layout.meta b/Runtime/RecyclerView/Layout.meta new file mode 100644 index 0000000..52779f5 --- /dev/null +++ b/Runtime/RecyclerView/Layout.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c2c36f0a0862000438099a52b9b5e69c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Layout/CircleLayoutManager.cs b/Runtime/RecyclerView/Layout/CircleLayoutManager.cs new file mode 100644 index 0000000..aedbb94 --- /dev/null +++ b/Runtime/RecyclerView/Layout/CircleLayoutManager.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + public class CircleLayoutManager : LayoutManager + { + private float radius; + private new CircleDirection direction; + private float intervalAngle; + private float initalAngle; + + public override RecyclerView RecyclerView + { + get => recyclerView; + set + { + recyclerView = value; + recyclerView.SetScroller(recyclerView.gameObject.AddComponent()); + } + } + + public CircleLayoutManager(CircleDirection direction = CircleDirection.Positive, float initalAngle = 0) + { + this.direction = direction; + this.initalAngle = initalAngle; + } + + public override Vector2 CalculateContentSize() + { + Vector2 size = viewProvider.CalculateViewSize(0); + radius = (Mathf.Min(viewportSize.x, viewportSize.y) - Mathf.Min(size.x, size.y)) / 2f - Mathf.Max(padding.x, padding.y); + intervalAngle = adapter.GetItemCount() > 0 ? 360f / adapter.GetItemCount() : 0; + + return viewportSize; + } + + public override Vector2 CalculateContentOffset() + { + return Vector2.zero; + } + + public override Vector2 CalculateViewportOffset() + { + return Vector2.zero; + } + + public override void Layout(ViewHolder viewHolder, int index) + { + viewHolder.RectTransform.anchoredPosition3D = CalculatePosition(index); + } + + public override Vector2 CalculatePosition(int index) + { + float angle = index * intervalAngle; + angle = direction == CircleDirection.Positive ? angle : -angle; + angle += initalAngle + ScrollPosition; + float radian = angle * (Mathf.PI / 180f); + float x = radius * Mathf.Sin(radian); + float y = radius * Mathf.Cos(radian); + + return new Vector2(x, y); + } + + public override int GetStartIndex() + { + return 0; + } + + public override int GetEndIndex() + { + return adapter.GetItemCount() - 1; + } + + public override bool IsFullVisibleStart(int index) => false; + + public override bool IsFullInvisibleStart(int index) => false; + + public override bool IsFullVisibleEnd(int index) => false; + + public override bool IsFullInvisibleEnd(int index) => false; + + public override bool IsVisible(int index) => true; + + public override float IndexToPosition(int index) + { + float position = index * intervalAngle; + + return -position; + } + + public override int PositionToIndex(float position) + { + int index = Mathf.RoundToInt(position / intervalAngle); + return -index; + } + + public override void DoItemAnimation() + { + List viewHolders = viewProvider.ViewHolders; + for (int i = 0; i < viewHolders.Count; i++) + { + float angle = i * intervalAngle + initalAngle; + angle = direction == CircleDirection.Positive ? angle + ScrollPosition : angle - ScrollPosition; + float delta = (angle - initalAngle) % 360; + delta = delta < 0 ? delta + 360 : delta; + delta = delta > 180 ? 360 - delta : delta; + float scale = delta < intervalAngle ? (1.4f - delta / intervalAngle) : 1; + scale = Mathf.Max(scale, 1); + + viewHolders[i].RectTransform.localScale = Vector3.one * scale; + } + } + } + + public enum CircleDirection + { + Positive, + Negative + } +} diff --git a/Runtime/RecyclerView/Layout/CircleLayoutManager.cs.meta b/Runtime/RecyclerView/Layout/CircleLayoutManager.cs.meta new file mode 100644 index 0000000..1a48c5d --- /dev/null +++ b/Runtime/RecyclerView/Layout/CircleLayoutManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 158658be786595e46a209d2b2162b4e2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Layout/GridLayoutManager.cs b/Runtime/RecyclerView/Layout/GridLayoutManager.cs new file mode 100644 index 0000000..aa34f4f --- /dev/null +++ b/Runtime/RecyclerView/Layout/GridLayoutManager.cs @@ -0,0 +1,124 @@ +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + public class GridLayoutManager : LayoutManager + { + private Vector2 cellSize; + + public GridLayoutManager(int unit = 1) + { + this.unit = unit; + } + + public override Vector2 CalculateContentSize() + { + cellSize = viewProvider.CalculateViewSize(0); + + int row = Mathf.CeilToInt(adapter.GetItemCount() / (float)unit); + float len; + if (direction == Direction.Vertical) + { + len = row * (cellSize.y + spacing.y) - spacing.y; + return new Vector2(contentSize.x, len + padding.y * 2); + } + + len = row * (cellSize.x + spacing.x) - spacing.x; + return new Vector2(len, contentSize.y + padding.x * 2); + } + + public override Vector2 CalculatePosition(int index) + { + int row = index / unit; + int column = index % unit; + float x, y; + if (direction == Direction.Vertical) + { + x = column * (cellSize.x + spacing.x); + y = row * (cellSize.y + spacing.y) - ScrollPosition; + } + else + { + x = row * (cellSize.x + spacing.x) - ScrollPosition; + y = column * (cellSize.y + spacing.y); + } + + return new Vector2(x + padding.x, y + padding.y); + } + + public override Vector2 CalculateContentOffset() + { + float width, height; + if (alignment == Alignment.Center) + { + width = Mathf.Min(contentSize.x, viewportSize.x); + height = Mathf.Min(contentSize.y, viewportSize.y); + } + else + { + width = viewportSize.x; + height = viewportSize.y; + } + return new Vector2((width - cellSize.x) / 2, (height - cellSize.y) / 2); + } + + public override Vector2 CalculateViewportOffset() + { + float width, height; + if (alignment == Alignment.Center) + { + width = Mathf.Min(contentSize.x, viewportSize.x); + height = Mathf.Min(contentSize.y, viewportSize.y); + } + else + { + width = viewportSize.x; + height = viewportSize.y; + } + return new Vector2((width - cellSize.x) / 2, (height - cellSize.y) / 2); + } + + public override int GetStartIndex() + { + float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x; + int index = Mathf.FloorToInt(ScrollPosition / len) * unit; + return Mathf.Max(0, index); + } + + public override int GetEndIndex() + { + float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; + float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x; + int index = Mathf.FloorToInt((ScrollPosition + viewLength) / len) * unit; + return Mathf.Min(index, adapter.GetItemCount() - 1); + } + + public override float IndexToPosition(int index) + { + int row = index / unit; + float len, viewLength, position; + if (direction == Direction.Vertical) + { + len = row * (cellSize.y + spacing.y); + viewLength = viewportSize.y; + position = len + viewLength > contentSize.y ? contentSize.y - viewportSize.y : len; + } + else + { + len = row * (cellSize.x + spacing.x); + viewLength = viewportSize.x; + position = len + viewLength > contentSize.x ? contentSize.x - viewportSize.x : len; + } + + return position; + } + + public override int PositionToIndex(float position) + { + float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x; + int index = Mathf.RoundToInt(position / len); + + return index * unit; + } + } +} diff --git a/Runtime/RecyclerView/Layout/GridLayoutManager.cs.meta b/Runtime/RecyclerView/Layout/GridLayoutManager.cs.meta new file mode 100644 index 0000000..05ac471 --- /dev/null +++ b/Runtime/RecyclerView/Layout/GridLayoutManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7e4f9a076f276cd4a8db0055fd37e220 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Layout/ILayoutManager.cs b/Runtime/RecyclerView/Layout/ILayoutManager.cs new file mode 100644 index 0000000..6cfd075 --- /dev/null +++ b/Runtime/RecyclerView/Layout/ILayoutManager.cs @@ -0,0 +1,115 @@ +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + public interface ILayoutManager + { + /// + /// 滚动时,刷新整个页面的布局 + /// + void UpdateLayout(); + + /// + /// 为 ViewHolder 设置布局 + /// + /// + /// + void Layout(ViewHolder viewHolder, int index); + + /// + /// 设置 Content 大小 + /// + void SetContentSize(); + + /// + /// 计算 Content 的大小 + /// + /// + Vector2 CalculateContentSize(); + + /// + /// 计算第 index 个 ViewHolder 到顶部的距离 + /// + /// + /// + Vector2 CalculatePosition(int index); + + /// + /// 计算 ViewHolder 相对于内容长度的偏移 + /// + /// + Vector2 CalculateContentOffset(); + + /// + /// 计算 ViewHolder 相对于视口的偏移 + /// + /// + Vector2 CalculateViewportOffset(); + + /// + /// 获取当前显示的第一个 ViewHolder 下标 + /// + /// + int GetStartIndex(); + + /// + /// 获取当前显示的最后一个 ViewHolder 下标 + /// + /// + int GetEndIndex(); + + /// + /// 数据下标转换成在布局中对应的位置 + /// + /// + /// + float IndexToPosition(int index); + + /// + /// 在布局中的位置转换成数据下标 + /// + /// + /// + int PositionToIndex(float position); + + /// + /// 滚动时,item 对应的动画 + /// + void DoItemAnimation(); + + /// + /// 判断第一个 ViewHolder 是否完全可见 + /// + /// 数据的真实下标 + /// + bool IsFullVisibleStart(int index); + + /// + /// 判断第一个 ViewHolder 是否完全不可见 + /// + /// 数据的真实下标 + /// + bool IsFullInvisibleStart(int index); + + /// + /// 判定最后一个 ViewHolder 是否完全可见 + /// + /// + /// + bool IsFullVisibleEnd(int index); + + /// + /// 判定最后一个 ViewHolder 是否完全不可见 + /// + /// + /// + bool IsFullInvisibleEnd(int index); + + /// + /// 判定第 index ViewHolder是否可见 + /// + /// + /// + bool IsVisible(int index); + } +} diff --git a/Runtime/RecyclerView/Layout/ILayoutManager.cs.meta b/Runtime/RecyclerView/Layout/ILayoutManager.cs.meta new file mode 100644 index 0000000..ee6af99 --- /dev/null +++ b/Runtime/RecyclerView/Layout/ILayoutManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 09e4099cf54916a41b7da73277ab988a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Layout/LayoutManager.cs b/Runtime/RecyclerView/Layout/LayoutManager.cs new file mode 100644 index 0000000..c5949d6 --- /dev/null +++ b/Runtime/RecyclerView/Layout/LayoutManager.cs @@ -0,0 +1,230 @@ +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + public abstract class LayoutManager : ILayoutManager + { + protected Vector2 viewportSize; + public Vector2 ViewportSize + { + get => viewportSize; + private set => viewportSize = value; + } + + protected Vector2 contentSize; + public Vector2 ContentSize + { + get => contentSize; + private set => contentSize = value; + } + + protected Vector2 contentOffset; + public Vector2 ContentOffset + { + get => contentOffset; + private set => contentOffset = value; + } + + protected Vector2 viewportOffset; + public Vector2 ViewportOffset + { + get => viewportOffset; + private set => viewportOffset = value; + } + + protected IAdapter adapter; + public IAdapter Adapter + { + get => adapter; + set => adapter = value; + } + + protected ViewProvider viewProvider; + public ViewProvider ViewProvider + { + get => viewProvider; + set => viewProvider = value; + } + + protected RecyclerView recyclerView; + public virtual RecyclerView RecyclerView + { + get => recyclerView; + set => recyclerView = value; + } + + protected Direction direction; + public Direction Direction + { + get => direction; + set => direction = value; + } + + protected Alignment alignment; + public Alignment Alignment + { + get => alignment; + set => alignment = value; + } + + protected Vector2 spacing; + public Vector2 Spacing + { + get => spacing; + set => spacing = value; + } + + protected Vector2 padding; + public Vector2 Padding + { + get => padding; + set => padding = value; + } + + protected int unit = 1; + public int Unit + { + get => unit; + set => unit = value; + } + + protected bool canScroll; + public bool CanScroll + { + get => canScroll; + set => canScroll = value; + } + + public float ScrollPosition => recyclerView.GetScrollPosition(); + + public LayoutManager() { } + + public void SetContentSize() + { + viewportSize = recyclerView.GetComponent().rect.size; + contentSize = CalculateContentSize(); + contentOffset = CalculateContentOffset(); + viewportOffset = CalculateViewportOffset(); + } + + public void UpdateLayout() + { + foreach (var viewHolder in viewProvider.ViewHolders) + { + Layout(viewHolder, viewHolder.Index); + } + } + + public virtual void Layout(ViewHolder viewHolder, int index) + { + Vector2 pos = CalculatePosition(index); + Vector3 position = direction == Direction.Vertical ? + new Vector3(pos.x - contentOffset.x, -pos.y + contentOffset.y, 0) : + new Vector3(pos.x - contentOffset.x, -pos.y + contentOffset.y, 0); + viewHolder.RectTransform.anchoredPosition3D = position; + } + + public abstract Vector2 CalculateContentSize(); + + public abstract Vector2 CalculatePosition(int index); + + public abstract Vector2 CalculateContentOffset(); + + public abstract Vector2 CalculateViewportOffset(); + + public abstract int GetStartIndex(); + + public abstract int GetEndIndex(); + + public abstract float IndexToPosition(int index); + + public abstract int PositionToIndex(float position); + + public virtual void DoItemAnimation() { } + + public virtual bool IsFullVisibleStart(int index) + { + Vector2 vector2 = CalculatePosition(index); + float position = direction == Direction.Vertical ? vector2.y : vector2.x; + return position + GetOffset() >= 0; + } + + public virtual bool IsFullInvisibleStart(int index) + { + Vector2 vector2 = CalculatePosition(index + unit); + float position = direction == Direction.Vertical ? vector2.y : vector2.x; + return position + GetOffset() < 0; + } + + public virtual bool IsFullVisibleEnd(int index) + { + Vector2 vector2 = CalculatePosition(index + unit); + float position = direction == Direction.Vertical ? vector2.y : vector2.x; + float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; + return position + GetOffset() <= viewLength; + } + + public virtual bool IsFullInvisibleEnd(int index) + { + Vector2 vector2 = CalculatePosition(index); + float position = direction == Direction.Vertical ? vector2.y : vector2.x; + float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; + return position + GetOffset() > viewLength; + } + + public virtual bool IsVisible(int index) + { + float position, viewLength; + viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; + + Vector2 vector2 = CalculatePosition(index); + position = direction == Direction.Vertical ? vector2.y : vector2.x; + if (position + GetOffset() > 0 && position + GetOffset() <= viewLength) + { + return true; + } + + vector2 = CalculatePosition(index + unit); + position = direction == Direction.Vertical ? vector2.y : vector2.x; + if (position + GetOffset() > 0 && position + GetOffset() <= viewLength) + { + return true; + } + + return false; + } + + protected virtual float GetFitContentSize() + { + float len; + if (direction == Direction.Vertical) + { + len = alignment == Alignment.Center ? Mathf.Min(contentSize.y, viewportSize.y) : viewportSize.y; + } + else + { + len = alignment == Alignment.Center ? Mathf.Min(contentSize.x, viewportSize.x) : viewportSize.x; + } + return len; + } + + protected virtual float GetOffset() + { + return direction == Direction.Vertical ? -contentOffset.y + viewportOffset.y : -contentOffset.x + viewportOffset.x; + } + } + + public enum Direction + { + Vertical = 1, + Horizontal = 2, + Custom = 10 + } + + public enum Alignment + { + Left, + Center, + Top + } +} diff --git a/Runtime/RecyclerView/Layout/LayoutManager.cs.meta b/Runtime/RecyclerView/Layout/LayoutManager.cs.meta new file mode 100644 index 0000000..396c4ac --- /dev/null +++ b/Runtime/RecyclerView/Layout/LayoutManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 959827c7e00900b47844bd9482d829ef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Layout/LinearLayoutManager.cs b/Runtime/RecyclerView/Layout/LinearLayoutManager.cs new file mode 100644 index 0000000..95e5716 --- /dev/null +++ b/Runtime/RecyclerView/Layout/LinearLayoutManager.cs @@ -0,0 +1,102 @@ +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + public class LinearLayoutManager : LayoutManager + { + protected float lineHeight; + + public LinearLayoutManager() { } + + public override Vector2 CalculateContentSize() + { + Vector2 size = viewProvider.CalculateViewSize(0); + lineHeight = direction == Direction.Vertical ? size.y : size.x; + + int index = adapter.GetItemCount(); + float position; + if (direction == Direction.Vertical) + { + position = index * (lineHeight + spacing.y) - spacing.y; + return new Vector2(contentSize.x, position + padding.y * 2); + } + position = index * (lineHeight + spacing.x) - spacing.x; + return new Vector2(position + padding.x * 2, contentSize.y); + } + + public override Vector2 CalculatePosition(int index) + { + float position; + if (direction == Direction.Vertical) + { + position = index * (lineHeight + spacing.y) - ScrollPosition; + return new Vector2(0, position + padding.y); + } + position = index * (lineHeight + spacing.x) - ScrollPosition; + return new Vector2(position + padding.x, 0); + } + + public override Vector2 CalculateContentOffset() + { + float len = GetFitContentSize(); + if (direction == Direction.Vertical) + { + return new Vector2(0, (len - lineHeight) / 2); + } + return new Vector2((len - lineHeight) / 2, 0); + } + + public override Vector2 CalculateViewportOffset() + { + if (direction == Direction.Vertical) + { + return new Vector2(0, (viewportSize.y - lineHeight) / 2); + } + return new Vector2((viewportSize.x - lineHeight) / 2, 0); + } + + public override int GetStartIndex() + { + float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; + int index = Mathf.FloorToInt(ScrollPosition / len); + return Mathf.Max(0, index); + } + + public override int GetEndIndex() + { + float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; + float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; + int index = Mathf.FloorToInt((ScrollPosition + viewLength) / len); + return Mathf.Min(index, adapter.GetItemCount() - 1); + } + + public override float IndexToPosition(int index) + { + if (index < 0 || index >= adapter.GetItemCount()) return 0; + + float len, viewLength, position; + if (direction == Direction.Vertical) + { + len = index * (lineHeight + spacing.y); + viewLength = viewportSize.y; + position = len + viewLength > contentSize.y ? contentSize.y - viewportSize.y : len; + } + else + { + len = index * (lineHeight + spacing.x); + viewLength = viewportSize.x; + position = len + viewLength > contentSize.x ? contentSize.x - viewportSize.x : len; + } + + return position; + } + + public override int PositionToIndex(float position) + { + float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; + int index = Mathf.RoundToInt(position / len); + + return index; + } + } +} diff --git a/Runtime/RecyclerView/Layout/LinearLayoutManager.cs.meta b/Runtime/RecyclerView/Layout/LinearLayoutManager.cs.meta new file mode 100644 index 0000000..7f27d73 --- /dev/null +++ b/Runtime/RecyclerView/Layout/LinearLayoutManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 36d9270c4a025694397715d3c16cde7e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Layout/MixedLayoutManager.cs b/Runtime/RecyclerView/Layout/MixedLayoutManager.cs new file mode 100644 index 0000000..4230c20 --- /dev/null +++ b/Runtime/RecyclerView/Layout/MixedLayoutManager.cs @@ -0,0 +1,136 @@ +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + public class MixedLayoutManager : LayoutManager + { + public MixedLayoutManager() { } + + public override Vector2 CalculateContentSize() + { + int index = adapter.GetItemCount(); + float position = 0; + for (int i = 0; i < index; i++) + { + position += GetLength(i); + } + + return direction == Direction.Vertical ? + new Vector2(contentSize.x, position - spacing.y + padding.y * 2) : + new Vector2(position - spacing.x + padding.x * 2, contentSize.y); + } + + public override Vector2 CalculatePosition(int index) + { + // TODO 优化点,将 position 定义成全局变量 + float position = 0; + for (int i = 0; i < index; i++) + { + position += GetLength(i); + } + position -= ScrollPosition; + return direction == Direction.Vertical ? new Vector2(0, position + padding.y) : new Vector2(position + padding.x, 0); + } + + public override Vector2 CalculateContentOffset() + { + Vector2 size = viewProvider.CalculateViewSize(0); + float len = GetFitContentSize(); + if (direction == Direction.Vertical) + { + return new Vector2(0, (len - size.y) / 2); + } + return new Vector2((len - size.x) / 2, 0); + } + + public override Vector2 CalculateViewportOffset() + { + Vector2 size = viewProvider.CalculateViewSize(0); + if (direction == Direction.Vertical) + { + return new Vector2(0, (viewportSize.y - size.y) / 2); + } + return new Vector2((viewportSize.x - size.x) / 2, 0); + } + + public override int GetStartIndex() + { + float position = 0; + float contentPosition = ScrollPosition; + int itemCount = adapter.GetItemCount(); + for (int i = 0; i < itemCount; i++) + { + position += GetLength(i); + + if (position > contentPosition) + { + return Mathf.Max(0, i); + } + } + return 0; + } + + public override int GetEndIndex() + { + float position = 0; + float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; + int itemCount = adapter.GetItemCount(); + for (int i = 0; i < itemCount; i++) + { + position += GetLength(i); + + if (position > ScrollPosition + viewLength) + { + return Mathf.Min(i, adapter.GetItemCount() - 1); ; + } + } + return itemCount - 1; + } + + private float GetLength(int index) + { + Vector2 size = viewProvider.CalculateViewSize(index); + if (index < adapter.GetItemCount() - 1) + { + size += spacing; + } + float len = direction == Direction.Vertical ? size.y : size.x; + return len; + } + + public override float IndexToPosition(int index) + { + Vector2 position = CalculatePosition(index); + if (direction == Direction.Vertical) + { + position.y = Mathf.Max(0, position.y); + position.y = Mathf.Min(position.y, contentSize.y - viewportSize.y); + return position.y; + } + else + { + position.x = Mathf.Max(0, position.x); + position.x = Mathf.Min(position.x, contentSize.x - viewportSize.x); + return position.x; + } + } + + public override int PositionToIndex(float position) + { + float len = 0; + + int itemCount = adapter.GetItemCount(); + for (int i = 0; i < itemCount; i++) + { + len += GetLength(i); + + if (len >= position) + { + return i; + } + } + + return 0; + } + } +} diff --git a/Runtime/RecyclerView/Layout/MixedLayoutManager.cs.meta b/Runtime/RecyclerView/Layout/MixedLayoutManager.cs.meta new file mode 100644 index 0000000..e3b553f --- /dev/null +++ b/Runtime/RecyclerView/Layout/MixedLayoutManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d296ceb3ab7786f43a94281a728d5205 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Layout/PageLayoutManager.cs b/Runtime/RecyclerView/Layout/PageLayoutManager.cs new file mode 100644 index 0000000..d1348a4 --- /dev/null +++ b/Runtime/RecyclerView/Layout/PageLayoutManager.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + public class PageLayoutManager : LinearLayoutManager + { + private float minScale; + + public PageLayoutManager(float minScale = 0.9f) + { + this.minScale = minScale; + } + + public override Vector2 CalculateContentSize() + { + Vector2 size = viewProvider.CalculateViewSize(0); + lineHeight = direction == Direction.Vertical ? size.y : size.x; + + int index = adapter.GetItemCount(); + float position; + if (direction == Direction.Vertical) + { + position = index * (lineHeight + spacing.y) - spacing.y; + position += viewportSize.y - lineHeight; + return new Vector2(contentSize.x, position + padding.y * 2); + } + position = index * (lineHeight + spacing.x) - spacing.x; + position += viewportSize.x - lineHeight; + return new Vector2(position + padding.x * 2, contentSize.y); + } + + public override Vector2 CalculatePosition(int index) + { + float position; + if (direction == Direction.Vertical) + { + position = index * (lineHeight + spacing.y) - ScrollPosition; + return new Vector2(0, position + padding.y); + } + position = index * (lineHeight + spacing.x) - ScrollPosition; + return new Vector2(position + padding.x, 0); + } + + public override Vector2 CalculateContentOffset() + { + return Vector2.zero; + } + + public override Vector2 CalculateViewportOffset() + { + return Vector2.zero; + } + + protected override float GetOffset() + { + float offset = direction == Direction.Vertical ? viewportSize.y - lineHeight : viewportSize.x - lineHeight; + return offset / 2; + } + + public override int PositionToIndex(float position) + { + float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; + float pos = IndexToPosition(recyclerView.CurrentIndex); + // 根据是前划还是后划,来加减偏移量 + int index = position > pos ? Mathf.RoundToInt(position / len + 0.25f) : Mathf.RoundToInt(position / len - 0.25f); + + return index; + } + + public override void DoItemAnimation() + { + List viewHolders = viewProvider.ViewHolders; + for (int i = 0; i < viewHolders.Count; i++) + { + float viewPos = direction == Direction.Vertical ? + -viewHolders[i].RectTransform.anchoredPosition.y : + viewHolders[i].RectTransform.anchoredPosition.x; + float scale = 1 - Mathf.Min(Mathf.Abs(viewPos) * 0.0006f, 1f); + scale = Mathf.Max(scale, minScale); + + viewHolders[i].RectTransform.localScale = Vector3.one * scale; + } + } + } +} diff --git a/Runtime/RecyclerView/Layout/PageLayoutManager.cs.meta b/Runtime/RecyclerView/Layout/PageLayoutManager.cs.meta new file mode 100644 index 0000000..718aa4b --- /dev/null +++ b/Runtime/RecyclerView/Layout/PageLayoutManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4fccdae6dae870c42bc27f435a94e621 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool.meta b/Runtime/RecyclerView/ObjectPool.meta new file mode 100644 index 0000000..cc7f7db --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a22aa56c3da6d724e97aafd9f3e7ed67 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/IMixedObjectFactory.cs b/Runtime/RecyclerView/ObjectPool/IMixedObjectFactory.cs new file mode 100644 index 0000000..547a0a5 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IMixedObjectFactory.cs @@ -0,0 +1,11 @@ + +public interface IMixedObjectFactory where T : class +{ + T Create(string typeName); + + void Destroy(string typeName, T obj); + + void Reset(string typeName, T obj); + + bool Validate(string typeName, T obj); +} diff --git a/Runtime/RecyclerView/ObjectPool/IMixedObjectFactory.cs.meta b/Runtime/RecyclerView/ObjectPool/IMixedObjectFactory.cs.meta new file mode 100644 index 0000000..9fcb864 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IMixedObjectFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 30f89ee61eb3f0949b5300bd9b7cc577 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/IMixedObjectPool.cs b/Runtime/RecyclerView/ObjectPool/IMixedObjectPool.cs new file mode 100644 index 0000000..d30a4fb --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IMixedObjectPool.cs @@ -0,0 +1,8 @@ +using System; + +public interface IMixedObjectPool : IDisposable where T : class +{ + T Allocate(string typeName); + + void Free(string typeName, T obj); +} diff --git a/Runtime/RecyclerView/ObjectPool/IMixedObjectPool.cs.meta b/Runtime/RecyclerView/ObjectPool/IMixedObjectPool.cs.meta new file mode 100644 index 0000000..209e188 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IMixedObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3f4c13a4827ebad4a9ff08e636fbc67e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/IObjectFactory.cs b/Runtime/RecyclerView/ObjectPool/IObjectFactory.cs new file mode 100644 index 0000000..ab3d6b5 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IObjectFactory.cs @@ -0,0 +1,28 @@ + +public interface IObjectFactory where T : class +{ + /// + /// 创建对象 + /// + /// + T Create(); + + /// + /// 销毁对象 + /// + /// + void Destroy(T obj); + + /// + /// 重置对象 + /// + /// + void Reset(T obj); + + /// + /// 验证对象 + /// + /// + /// + bool Validate(T obj); +} diff --git a/Runtime/RecyclerView/ObjectPool/IObjectFactory.cs.meta b/Runtime/RecyclerView/ObjectPool/IObjectFactory.cs.meta new file mode 100644 index 0000000..0e5fc88 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IObjectFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 77a642084db01624c8e5876605195d49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/IObjectPool.cs b/Runtime/RecyclerView/ObjectPool/IObjectPool.cs new file mode 100644 index 0000000..af1f544 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IObjectPool.cs @@ -0,0 +1,23 @@ +using System; + +public interface IObjectPool : IDisposable +{ + /// + /// 从池子中分配一个可用对象,没有的话就创建一个 + /// + /// + object Allocate(); + + /// + /// 将对象回收到池子中去,如果池中的对象数量已经超过了 maxSize,则直接销毁该对象 + /// + /// + void Free(object obj); +} + +public interface IObjectPool : IObjectPool, IDisposable where T : class +{ + new T Allocate(); + + void Free(T obj); +} diff --git a/Runtime/RecyclerView/ObjectPool/IObjectPool.cs.meta b/Runtime/RecyclerView/ObjectPool/IObjectPool.cs.meta new file mode 100644 index 0000000..5debf3d --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd12164a5eea20e41bf7f3b7704f4b33 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/IPooledObject.cs b/Runtime/RecyclerView/ObjectPool/IPooledObject.cs new file mode 100644 index 0000000..cfbfa17 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IPooledObject.cs @@ -0,0 +1,5 @@ + +public interface IPooledObject +{ + void Free(); +} diff --git a/Runtime/RecyclerView/ObjectPool/IPooledObject.cs.meta b/Runtime/RecyclerView/ObjectPool/IPooledObject.cs.meta new file mode 100644 index 0000000..ee50c4b --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/IPooledObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b174ccb64b3938c449d4a69a3262d8d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/MixedObjectPool.cs b/Runtime/RecyclerView/ObjectPool/MixedObjectPool.cs new file mode 100644 index 0000000..60484e6 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/MixedObjectPool.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +public class MixedObjectPool : IMixedObjectPool where T : class +{ + private const int DEFAULT_MAX_SIZE_PER_TYPE = 10; + + private readonly ConcurrentDictionary> entries; + private readonly ConcurrentDictionary typeSize; + private readonly IMixedObjectFactory factory; + + private int defaultMaxSizePerType; + + public MixedObjectPool(IMixedObjectFactory factory) : this(factory, DEFAULT_MAX_SIZE_PER_TYPE) + { + } + + public MixedObjectPool(IMixedObjectFactory factory, int defaultMaxSizePerType) + { + this.factory = factory; + this.defaultMaxSizePerType = defaultMaxSizePerType; + + if (defaultMaxSizePerType <= 0) + { + throw new ArgumentException("The maxSize must be greater than 0."); + } + + entries = new ConcurrentDictionary>(); + typeSize = new ConcurrentDictionary(); + } + + public T Allocate(string typeName) + { + if (entries.TryGetValue(typeName, out List list) && list.Count > 0) + { + T obj = list[0]; + list.RemoveAt(0); + return obj; + } + return factory.Create(typeName); + } + + public void Free(string typeName, T obj) + { + if (obj == null) return; + + if (!factory.Validate(typeName, obj)) + { + factory.Destroy(typeName, obj); + return; + } + + int maxSize = GetMaxSize(typeName); + List list = entries.GetOrAdd(typeName, n => new List()); + if (list.Count >= maxSize) + { + factory.Destroy(typeName, obj); + return; + } + + factory.Reset(typeName, obj); + list.Add(obj); + } + + public int GetMaxSize(string typeName) + { + if (typeSize.TryGetValue(typeName, out int size)) + { + return size; + } + return defaultMaxSizePerType; + } + + public void SetMaxSize(string typeName, int value) + { + typeSize.AddOrUpdate(typeName, value, (key, oldValue) => value); + } + + protected virtual void Clear() + { + foreach (var kv in entries) + { + string typeName = kv.Key; + List list = kv.Value; + + if (list == null || list.Count <= 0) continue; + + list.ForEach(e => factory.Destroy(typeName, e)); + list.Clear(); + } + entries.Clear(); + typeSize.Clear(); + } + + public void Dispose() + { + Clear(); + GC.SuppressFinalize(this); + } +} diff --git a/Runtime/RecyclerView/ObjectPool/MixedObjectPool.cs.meta b/Runtime/RecyclerView/ObjectPool/MixedObjectPool.cs.meta new file mode 100644 index 0000000..5af2b10 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/MixedObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a62e68f43eac4b419140191eb09ea56 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/ObjectPool.cs b/Runtime/RecyclerView/ObjectPool/ObjectPool.cs new file mode 100644 index 0000000..1476ef5 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/ObjectPool.cs @@ -0,0 +1,108 @@ +using System; +using System.Threading; + +public class ObjectPool : IObjectPool where T : class +{ + private int maxSize; + private int initialSize; + protected readonly T[] entries = null; + protected readonly IObjectFactory factory; + + public ObjectPool(IObjectFactory factory) : this(factory, Environment.ProcessorCount * 2) + { + } + + public ObjectPool(IObjectFactory factory, int maxSize) : this(factory, 0, maxSize) + { + } + + public ObjectPool(IObjectFactory factory, int initialSize, int maxSize) + { + this.factory = factory; + this.initialSize = initialSize; + this.maxSize = maxSize; + this.entries = new T[maxSize]; + + if (maxSize < initialSize) + { + throw new ArgumentException("The maxSize must be greater than or equal to the initialSize."); + } + + for (int i = 0; i < initialSize; i++) + { + entries[i] = factory.Create(); + } + } + + public int MaxSize => maxSize; + + public int InitialSize => initialSize; + + public virtual T Allocate() + { + for (var i = 0; i < entries.Length; i++) + { + T value = entries[i]; + if (value == null) continue; + + if (Interlocked.CompareExchange(ref entries[i], null, value) == value) + { + return value; + } + } + + return factory.Create(); + } + + public virtual void Free(T obj) + { + if (obj == null) return; + + if (!factory.Validate(obj)) + { + factory.Destroy(obj); + return; + } + + factory.Reset(obj); + + for (var i = 0; i < entries.Length; i++) + { + if (Interlocked.CompareExchange(ref entries[i], obj, null) == null) + { + return; + } + } + + factory.Destroy(obj); + } + + object IObjectPool.Allocate() + { + return Allocate(); + } + + public void Free(object obj) + { + Free((T)obj); + } + + protected virtual void Clear() + { + for (var i = 0; i < entries.Length; i++) + { + var value = Interlocked.Exchange(ref entries[i], null); + + if (value != null) + { + factory.Destroy(value); + } + } + } + + public void Dispose() + { + Clear(); + GC.SuppressFinalize(this); + } +} diff --git a/Runtime/RecyclerView/ObjectPool/ObjectPool.cs.meta b/Runtime/RecyclerView/ObjectPool/ObjectPool.cs.meta new file mode 100644 index 0000000..543d2f4 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/ObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 30999e5e03e2b434996100b09960468f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/UnityComponentFactory.cs b/Runtime/RecyclerView/ObjectPool/UnityComponentFactory.cs new file mode 100644 index 0000000..c27b24c --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/UnityComponentFactory.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +public class UnityComponentFactory : IObjectFactory where T : Component +{ + private T template; + private Transform parent; + + public UnityComponentFactory(T template, Transform parent) + { + this.template = template; + this.parent = parent; + } + + public T Create() + { + T obj = Object.Instantiate(template, parent); + return obj; + } + + public void Destroy(T obj) + { + Object.Destroy(obj.gameObject); + } + + public void Reset(T obj) + { + obj.gameObject.SetActive(false); + obj.gameObject.transform.position = Vector3.zero; + obj.gameObject.transform.rotation = Quaternion.identity; + } + + public bool Validate(T obj) + { + return true; + } +} diff --git a/Runtime/RecyclerView/ObjectPool/UnityComponentFactory.cs.meta b/Runtime/RecyclerView/ObjectPool/UnityComponentFactory.cs.meta new file mode 100644 index 0000000..956f9ab --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/UnityComponentFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3e2e3a0444783a7469d494ad630dc705 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/UnityGameObjectFactory.cs b/Runtime/RecyclerView/ObjectPool/UnityGameObjectFactory.cs new file mode 100644 index 0000000..58f56d1 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/UnityGameObjectFactory.cs @@ -0,0 +1,35 @@ +using UnityEngine; + +public class UnityGameObjectFactory : IObjectFactory +{ + protected GameObject template; + protected Transform parent; + + public UnityGameObjectFactory(GameObject template, Transform parent) + { + this.template = template; + this.parent = parent; + } + + public virtual GameObject Create() + { + return Object.Instantiate(template, parent); + } + + public virtual void Reset(GameObject obj) + { + obj.SetActive(false); + obj.transform.position = Vector3.zero; + obj.transform.rotation = Quaternion.identity; + } + + public virtual void Destroy(GameObject obj) + { + Object.Destroy(obj); + } + + public virtual bool Validate(GameObject obj) + { + return true; + } +} diff --git a/Runtime/RecyclerView/ObjectPool/UnityGameObjectFactory.cs.meta b/Runtime/RecyclerView/ObjectPool/UnityGameObjectFactory.cs.meta new file mode 100644 index 0000000..f77bddc --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/UnityGameObjectFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7373bce96f2a515499c52b060ad9e01e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/UnityMixedComponentFactory.cs b/Runtime/RecyclerView/ObjectPool/UnityMixedComponentFactory.cs new file mode 100644 index 0000000..3b8d7b2 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/UnityMixedComponentFactory.cs @@ -0,0 +1,61 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class UnityMixedComponentFactory : IMixedObjectFactory where T : Component +{ + protected T template; + protected Transform parent; + protected List list; + + private Dictionary dict = new(); + + public UnityMixedComponentFactory(T template, Transform parent) + { + this.template = template; + this.parent = parent; + } + + public UnityMixedComponentFactory(List list, Transform parent) + { + this.list = list; + this.parent = parent; + + foreach (var data in list) + { + dict[data.name] = data; + } + } + + public UnityMixedComponentFactory(Dictionary dict, Transform parent) + { + this.dict = dict; + this.parent = parent; + } + + public T Create(string typeName) + { + T obj = Object.Instantiate(dict[typeName], parent); + obj.transform.position = Vector3.zero; + obj.transform.rotation = Quaternion.identity; + + return obj; + } + + public void Destroy(string typeName, T obj) + { + Object.Destroy(obj.gameObject); + } + + public void Reset(string typeName, T obj) + { + obj.gameObject.SetActive(false); + obj.transform.position = Vector3.zero; + obj.transform.rotation = Quaternion.identity; + } + + public bool Validate(string typeName, T obj) + { + return true; + } +} diff --git a/Runtime/RecyclerView/ObjectPool/UnityMixedComponentFactory.cs.meta b/Runtime/RecyclerView/ObjectPool/UnityMixedComponentFactory.cs.meta new file mode 100644 index 0000000..19b9f96 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/UnityMixedComponentFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5c2d58d006be3fc458b8c6761d76bc88 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ObjectPool/UnityMixedGameObjectFactory.cs b/Runtime/RecyclerView/ObjectPool/UnityMixedGameObjectFactory.cs new file mode 100644 index 0000000..585ac75 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/UnityMixedGameObjectFactory.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +public class UnityMixedGameObjectFactory : IMixedObjectFactory +{ + protected GameObject template; + protected Transform parent; + + public UnityMixedGameObjectFactory(GameObject template, Transform parent) + { + this.template = template; + this.parent = parent; + } + + public GameObject Create(string typeName) + { + GameObject obj = Object.Instantiate(template, parent); + GameObject model = Object.Instantiate(Resources.Load("ObjectPools/" + typeName), obj.transform); + model.transform.position = Vector3.zero; + model.transform.rotation = Quaternion.identity; + + return obj; + } + + public void Destroy(string typeName, GameObject obj) + { + Object.Destroy(obj); + } + + public void Reset(string typeName, GameObject obj) + { + obj.SetActive(false); + obj.transform.position = Vector3.zero; + obj.transform.rotation = Quaternion.identity; + } + + public bool Validate(string typeName, GameObject obj) + { + return true; + } +} diff --git a/Runtime/RecyclerView/ObjectPool/UnityMixedGameObjectFactory.cs.meta b/Runtime/RecyclerView/ObjectPool/UnityMixedGameObjectFactory.cs.meta new file mode 100644 index 0000000..4f53e07 --- /dev/null +++ b/Runtime/RecyclerView/ObjectPool/UnityMixedGameObjectFactory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 16c8995b85215c6458119d581eea60b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/RecyclerView.cs b/Runtime/RecyclerView/RecyclerView.cs new file mode 100644 index 0000000..451027e --- /dev/null +++ b/Runtime/RecyclerView/RecyclerView.cs @@ -0,0 +1,369 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace AlicizaX.UI.RecyclerView +{ + public class RecyclerView : MonoBehaviour + { + [SerializeField] private Direction direction; + public Direction Direction + { + get => direction; + set => direction = value; + } + + [SerializeField] private Alignment alignment; + public Alignment Alignment + { + get => alignment; + set => alignment = value; + } + + [SerializeField] private Vector2 spacing; + public Vector2 Spacing + { + get => spacing; + set => spacing = value; + } + + [SerializeField] private Vector2 padding; + public Vector2 Padding + { + get => padding; + set => padding = value; + } + + [SerializeField] private bool scroll; + public bool Scroll + { + get => scroll; + set => scroll = value; + } + + [SerializeField] private bool snap; + public bool Snap + { + get => snap; + set => snap = value & scroll; + } + + [SerializeField, Range(1f, 50f)] private float scrollSpeed = 7f; + public float ScrollSpeed + { + get => scrollSpeed; + set => scrollSpeed = value; + } + + [SerializeField, Range(10f, 50f)] private float wheelSpeed = 30f; + public float WheeelSpeed + { + get => wheelSpeed; + set => wheelSpeed = value; + } + + [SerializeField] private ViewHolder[] templates; + public ViewHolder[] Templates + { + get => templates; + set => templates = value; + } + + private ViewProvider viewProvider; + private LayoutManager layoutManager; + private Scroller scroller; + private Scrollbar scrollbar; + + private int startIndex, endIndex; + private int currentIndex; + public int CurrentIndex + { + get => currentIndex; + set => currentIndex = value; + } + + public bool CanScroll => true; + + private RectTransform content; + public RectTransform Content + { + get + { + if (content == null) + { + content = transform.GetChild(0).GetComponent(); + } + return content; + } + } + + public ViewProvider ViewProvider + { + get + { + viewProvider ??= templates.Length > 1 ? new MixedViewProvider(this, templates) : new SimpleViewProvider(this, templates); + return viewProvider; + } + } + + public Scroller Scroller + { + get + { + if (scroller == null) + { + if (scroll) + { + scroller = gameObject.AddComponent(); + ConfigScroller(); + } + } + return scroller; + } + } + + public Scrollbar Scrollbar + { + get + { + if (scrollbar == null) + { + scrollbar = GetComponentInChildren(); + if (scrollbar != null) + { + scrollbar.gameObject.SetActive(scroll); + scrollbar.onValueChanged.AddListener(OnScrollbarChanged); + scrollbar.gameObject.AddComponent().OnDragEnd = OnScrollbarDragEnd; + } + } + return scrollbar; + } + } + + public IAdapter Adapter { get; set; } + + public LayoutManager LayoutManager => layoutManager; + + public Action OnIndexChanged; + public Action OnScrollValueChanged; + + private void OnValidate() + { + if (scroller != null) + { + scroller.ScrollSpeed = scrollSpeed; + scroller.WheelSpeed = wheelSpeed; + } + } + + private void OnScrollChanged(float pos) + { + layoutManager.UpdateLayout(); + + if (Scrollbar != null) + { + Scrollbar.SetValueWithoutNotify(pos / Scroller.MaxPosition); + } + + if (layoutManager.IsFullInvisibleStart(startIndex)) + { + viewProvider.RemoveViewHolder(startIndex); + startIndex += layoutManager.Unit; + } + else if (layoutManager.IsFullVisibleStart(startIndex)) + { + if (startIndex == 0) + { + // TODO Do something, eg: Refresh + } + else + { + startIndex -= layoutManager.Unit; + viewProvider.CreateViewHolder(startIndex); + } + } + + if (layoutManager.IsFullInvisibleEnd(endIndex)) + { + viewProvider.RemoveViewHolder(endIndex); + endIndex -= layoutManager.Unit; + } + else if (layoutManager.IsFullVisibleEnd(endIndex)) + { + if (endIndex >= viewProvider.GetItemCount() - layoutManager.Unit) + { + // TODO Do something, eg: Load More + } + else + { + endIndex += layoutManager.Unit; + viewProvider.CreateViewHolder(endIndex); + } + } + + // 使用滚动条快速定位时,刷新整个列表 + if (!layoutManager.IsVisible(startIndex) || !layoutManager.IsVisible(endIndex)) + { + Refresh(); + } + + layoutManager.DoItemAnimation(); + + OnScrollValueChanged?.Invoke(); + } + + private void OnMoveStoped() + { + if (Snap) + { + SnapTo(); + } + } + + private void OnScrollbarChanged(float ratio) + { + Scroller.ScrollToRatio(ratio); + } + + private void OnScrollbarDragEnd() + { + if (Scroller.Position < Scroller.MaxPosition) + { + if (Snap) + { + SnapTo(); + } + } + } + + public void Reset() + { + viewProvider?.Reset(); + if (scroller != null) + { + scroller.Position = 0; + } + if (scrollbar != null) + { + scrollbar.SetValueWithoutNotify(0); + } + } + + public void SetLayoutManager(LayoutManager layoutManager) + { + this.layoutManager = layoutManager; + + ViewProvider.Adapter = Adapter; + ViewProvider.LayoutManager = layoutManager; + + this.layoutManager.RecyclerView = this; + this.layoutManager.Adapter = Adapter; + this.layoutManager.ViewProvider = viewProvider; + this.layoutManager.Direction = direction; + this.layoutManager.Alignment = alignment; + this.layoutManager.Spacing = spacing; + this.layoutManager.Padding = padding; + this.layoutManager.CanScroll = CanScroll; + } + + public void SetScroller(Scroller newScroller) + { + if (!scroll) return; + + if (scroller != null) + { + scroller.OnValueChanged.RemoveListener(OnScrollChanged); + scroller.OnMoveStoped.RemoveListener(OnMoveStoped); + Destroy(scroller); + } + + scroller = newScroller; + ConfigScroller(); + } + + private void ConfigScroller() + { + scroller.ScrollSpeed = scrollSpeed; + scroller.WheelSpeed = wheelSpeed; + scroller.Snap = Snap; + scroller.OnValueChanged.AddListener(OnScrollChanged); + scroller.OnMoveStoped.AddListener(OnMoveStoped); + } + + public void Refresh() + { + ViewProvider.Clear(); + + startIndex = layoutManager.GetStartIndex(); + endIndex = layoutManager.GetEndIndex(); + for (int i = startIndex; i <= endIndex; i += layoutManager.Unit) + { + ViewProvider.CreateViewHolder(i); + } + + layoutManager.DoItemAnimation(); + } + + public void RequestLayout() + { + layoutManager.SetContentSize(); + + if (Scroller == null) return; + + Scroller.Direction = direction; + Scroller.ViewSize = layoutManager.ViewportSize; + Scroller.ContentSize = layoutManager.ContentSize; + + if (Scrollbar != null && Scroller.ContentSize != Vector2.zero) + { + if ((direction == Direction.Vertical && layoutManager.ContentSize.y <= layoutManager.ViewportSize.y) || + (direction == Direction.Horizontal && layoutManager.ContentSize.x <= layoutManager.ViewportSize.x) || + (direction == Direction.Custom)) + { + Scrollbar.gameObject.SetActive(false); + } + else + { + Scrollbar.gameObject.SetActive(true); + Scrollbar.direction = direction == Direction.Vertical ? + Scrollbar.Direction.TopToBottom : + Scrollbar.Direction.LeftToRight; + Scrollbar.size = direction == Direction.Vertical ? + Scroller.ViewSize.y / Scroller.ContentSize.y : + Scroller.ViewSize.x / Scroller.ContentSize.x; + } + } + } + + public float GetScrollPosition() + { + return Scroller ? Scroller.Position : 0; + } + + public void ScrollTo(int index, bool smooth = false) + { + if (!scroll) return; + + Scroller.ScrollTo(layoutManager.IndexToPosition(index), smooth); + if (!smooth) + { + Refresh(); + } + + index %= Adapter.GetItemCount(); + index = index < 0 ? Adapter.GetItemCount() + index : index; + + if (currentIndex != index) + { + currentIndex = index; + OnIndexChanged?.Invoke(currentIndex); + } + } + + private void SnapTo() + { + var index = layoutManager.PositionToIndex(GetScrollPosition()); + ScrollTo(index, true); + } + } +} diff --git a/Runtime/RecyclerView/RecyclerView.cs.meta b/Runtime/RecyclerView/RecyclerView.cs.meta new file mode 100644 index 0000000..a7f6e1c --- /dev/null +++ b/Runtime/RecyclerView/RecyclerView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7efd8e83d2092b347952108134dc37eb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Scroller.meta b/Runtime/RecyclerView/Scroller.meta new file mode 100644 index 0000000..6dfaea3 --- /dev/null +++ b/Runtime/RecyclerView/Scroller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b79ba677d829b904794e3b4a09cb67c2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Scroller/CircleScroller.cs b/Runtime/RecyclerView/Scroller/CircleScroller.cs new file mode 100644 index 0000000..336e374 --- /dev/null +++ b/Runtime/RecyclerView/Scroller/CircleScroller.cs @@ -0,0 +1,61 @@ +using UnityEngine; +using UnityEngine.EventSystems; + +namespace AlicizaX.UI.RecyclerView +{ + public class CircleScroller : Scroller + { + private Vector2 centerPosition; + + private void Awake() + { + RectTransform rectTransform = GetComponent(); + Vector2 position = transform.position; + Vector2 size = rectTransform.sizeDelta; + + if (rectTransform.pivot.x == 0) + { + centerPosition.x = position.x + size.x / 2f; + } + else if (rectTransform.pivot.x == 0.5f) + { + centerPosition.x = position.x; + } + else + { + centerPosition.x = position.x - size.x / 2f; + } + + if (rectTransform.pivot.y == 0) + { + centerPosition.y = position.y + size.y / 2f; + } + else if (rectTransform.pivot.y == 0.5f) + { + centerPosition.y = position.y; + } + else + { + centerPosition.y = position.y - size.y / 2f; + } + } + + internal override float GetDelta(PointerEventData eventData) + { + float delta; + if (Mathf.Abs(eventData.delta.x) > Mathf.Abs(eventData.delta.y)) + { + delta = eventData.position.y > centerPosition.y ? eventData.delta.x : -eventData.delta.x; + } + else + { + delta = eventData.position.x < centerPosition.x ? eventData.delta.y : -eventData.delta.y; + } + return delta * 0.1f; + } + + protected override void Elastic() + { + } + } +} diff --git a/Runtime/RecyclerView/Scroller/CircleScroller.cs.meta b/Runtime/RecyclerView/Scroller/CircleScroller.cs.meta new file mode 100644 index 0000000..7b3cbd9 --- /dev/null +++ b/Runtime/RecyclerView/Scroller/CircleScroller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 85235544e637a4a4f93ad88a5cfdac0a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Scroller/IScroller.cs b/Runtime/RecyclerView/Scroller/IScroller.cs new file mode 100644 index 0000000..c42a4af --- /dev/null +++ b/Runtime/RecyclerView/Scroller/IScroller.cs @@ -0,0 +1,15 @@ +using UnityEngine.Events; + +namespace AlicizaX.UI.RecyclerView +{ + public interface IScroller + { + float Position { get; set; } + + void ScrollTo(float position, bool smooth = false); + } + + public class ScrollerEvent : UnityEvent { } + public class MoveStopEvent : UnityEvent { } + public class DraggingEvent : UnityEvent { } +} diff --git a/Runtime/RecyclerView/Scroller/IScroller.cs.meta b/Runtime/RecyclerView/Scroller/IScroller.cs.meta new file mode 100644 index 0000000..76227ac --- /dev/null +++ b/Runtime/RecyclerView/Scroller/IScroller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3928a884003099344b231a9d554dc5da +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Scroller/ScrollbarEx.cs b/Runtime/RecyclerView/Scroller/ScrollbarEx.cs new file mode 100644 index 0000000..f2266de --- /dev/null +++ b/Runtime/RecyclerView/Scroller/ScrollbarEx.cs @@ -0,0 +1,80 @@ +using System; +using PrimeTween; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.UI; + +namespace AlicizaX.UI.RecyclerView +{ + public class ScrollbarEx : MonoBehaviour, IBeginDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler + { + private RectTransform handle; + private Scrollbar scrollbar; + + public Action OnDragEnd; + + private bool dragging; + private bool hovering; + + private void Awake() + { + scrollbar = GetComponent(); + handle = scrollbar.handleRect; + } + + public void OnBeginDrag(PointerEventData eventData) + { + dragging = true; + } + + public void OnEndDrag(PointerEventData eventData) + { + dragging = false; + if (!hovering) + { + if (scrollbar.direction == Scrollbar.Direction.TopToBottom || + scrollbar.direction == Scrollbar.Direction.BottomToTop) + { + Tween.ScaleX(handle, 1f, 0.2f); + } + else + { + Tween.ScaleY(handle, 1f, 0.2f); + } + } + + OnDragEnd?.Invoke(); + } + + public void OnPointerEnter(PointerEventData eventData) + { + hovering = true; + if (scrollbar.direction == Scrollbar.Direction.TopToBottom || + scrollbar.direction == Scrollbar.Direction.BottomToTop) + { + Tween.ScaleX(handle, 2f, 0.2f); + } + else + { + Tween.ScaleY(handle, 2f, 0.2f); + } + } + + public void OnPointerExit(PointerEventData eventData) + { + hovering = false; + if (!dragging) + { + if (scrollbar.direction == Scrollbar.Direction.TopToBottom || + scrollbar.direction == Scrollbar.Direction.BottomToTop) + { + Tween.ScaleX(handle, 1f, 0.2f); + } + else + { + Tween.ScaleY(handle, 1f, 0.2f); + } + } + } + } +} diff --git a/Runtime/RecyclerView/Scroller/ScrollbarEx.cs.meta b/Runtime/RecyclerView/Scroller/ScrollbarEx.cs.meta new file mode 100644 index 0000000..5f0c2ad --- /dev/null +++ b/Runtime/RecyclerView/Scroller/ScrollbarEx.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 682cfe39b0fffe544be8d5c11eb369e5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/Scroller/Scroller.cs b/Runtime/RecyclerView/Scroller/Scroller.cs new file mode 100644 index 0000000..1349b4f --- /dev/null +++ b/Runtime/RecyclerView/Scroller/Scroller.cs @@ -0,0 +1,245 @@ +using System.Collections; +using UnityEngine; +using UnityEngine.EventSystems; + +namespace AlicizaX.UI.RecyclerView +{ + public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler + { + protected float position; + public float Position { get => position; set => position = value; } + + protected float velocity; + public float Velocity => velocity; + + protected Direction direction; + public Direction Direction + { + get => direction; + set => direction = value; + } + + /// + /// 内容所需要大小 + /// + protected Vector2 contentSize; + public Vector2 ContentSize + { + get => contentSize; + set => contentSize = value; + } + + /// + /// 所在 View 的真实大小 + /// + protected Vector2 viewSize; + public Vector2 ViewSize + { + get => viewSize; + set => viewSize = value; + } + + protected float scrollSpeed = 1f; + public float ScrollSpeed + { + get => scrollSpeed; + set => scrollSpeed = value; + } + + protected float wheelSpeed = 30f; + public float WheelSpeed + { + get => wheelSpeed; + set => wheelSpeed = value; + } + + protected bool snap; + public bool Snap + { + get => snap; + set => snap = value; + } + + protected ScrollerEvent scrollerEvent = new(); + protected MoveStopEvent moveStopEvent = new(); + protected DraggingEvent draggingEvent = new(); + + public float MaxPosition => direction == Direction.Vertical ? + Mathf.Max(contentSize.y - viewSize.y, 0) : + Mathf.Max(contentSize.x - viewSize.x, 0); + + public float ViewLength => direction == Direction.Vertical ? viewSize.y : viewSize.x; + + public ScrollerEvent OnValueChanged { get => scrollerEvent; set => scrollerEvent = value; } + + public MoveStopEvent OnMoveStoped { get => moveStopEvent; set => moveStopEvent = value; } + + public DraggingEvent OnDragging { get => draggingEvent; set => draggingEvent = value; } + + // 停止滑动的时间,但此时并未释放鼠标按键 + private float dragStopTime = 0f; + + public virtual void ScrollTo(float position, bool smooth = false) + { + if (position == this.position) return; + + if (!smooth) + { + this.position = position; + OnValueChanged?.Invoke(this.position); + } + else + { + StopAllCoroutines(); + StartCoroutine(MoveTo(position)); + } + } + + public virtual void ScrollToRatio(float ratio) + { + ScrollTo(MaxPosition * ratio, false); + } + + public void OnBeginDrag(PointerEventData eventData) + { + OnDragging?.Invoke(true); + StopAllCoroutines(); + } + + public void OnEndDrag(PointerEventData eventData) + { + Inertia(); + Elastic(); + OnDragging?.Invoke(false); + } + + public void OnDrag(PointerEventData eventData) + { + dragStopTime = Time.time; + + velocity = GetDelta(eventData); + position += velocity; + + OnValueChanged?.Invoke(position); + } + + public void OnScroll(PointerEventData eventData) + { + StopAllCoroutines(); + + float rate = GetScrollRate() * wheelSpeed; + velocity = direction == Direction.Vertical ? -eventData.scrollDelta.y * rate : eventData.scrollDelta.x * rate; + position += velocity; + + OnValueChanged?.Invoke(position); + + Elastic(); + } + + internal virtual float GetDelta(PointerEventData eventData) + { + float rate = GetScrollRate(); + return direction == Direction.Vertical ? eventData.delta.y * rate : -eventData.delta.x * rate; + } + + private float GetScrollRate() + { + float rate = 1f; + if (position < 0) + { + rate = Mathf.Max(0, 1 - (Mathf.Abs(position) / ViewLength)); + } + else if (position > MaxPosition) + { + rate = Mathf.Max(0, 1 - (Mathf.Abs(position - MaxPosition) / ViewLength)); + } + return rate; + } + + /// + /// 松手时的惯性滑动 + /// + protected virtual void Inertia() + { + // 松手时的时间 离 停止滑动的时间 超过一定时间,则认为此次惯性滑动无效 + if (!snap && (Time.time - dragStopTime) > 0.01f) return; + + if (Mathf.Abs(velocity) > 0.1f) + { + StopAllCoroutines(); + StartCoroutine(InertiaTo()); + } + else + { + OnMoveStoped?.Invoke(); + } + } + + /// + /// 滑动到顶部/底部之后,松手时回弹 + /// + protected virtual void Elastic() + { + if (position < 0) + { + StopAllCoroutines(); + StartCoroutine(ElasticTo(0)); + } + else if (position > MaxPosition) + { + StopAllCoroutines(); + StartCoroutine(ElasticTo(MaxPosition)); + } + } + + IEnumerator InertiaTo() + { + float timer = 0f; + float p = position; + float v = velocity > 0 ? Mathf.Min(velocity, 100) : Mathf.Max(velocity, -100); + float duration = snap ? 0.1f : 1f; + while (timer < duration) + { + float y = (float)EaseUtil.EaseOutCirc(timer) * 40; + timer += Time.deltaTime; + position = p + y * v; + + Elastic(); + + OnValueChanged?.Invoke(position); + + yield return new WaitForEndOfFrame(); + } + + OnMoveStoped?.Invoke(); + } + + IEnumerator ElasticTo(float targetPos) + { + yield return ToPosition(targetPos, 7); + } + + IEnumerator MoveTo(float targetPos) + { + yield return ToPosition(targetPos, scrollSpeed); + } + + IEnumerator ToPosition(float targetPos, float speed) + { + float startPos = position; + float time = Time.deltaTime; + while (Mathf.Abs(targetPos - position) > 0.1f) + { + position = Mathf.Lerp(startPos, targetPos, time * speed); + OnValueChanged?.Invoke(position); + + time += Time.deltaTime; + + yield return new WaitForEndOfFrame(); + } + + position = targetPos; + OnValueChanged?.Invoke(position); + } + } +} diff --git a/Runtime/RecyclerView/Scroller/Scroller.cs.meta b/Runtime/RecyclerView/Scroller/Scroller.cs.meta new file mode 100644 index 0000000..d656bfc --- /dev/null +++ b/Runtime/RecyclerView/Scroller/Scroller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7b7de4cb3a1546e4a9ade6b8dbf8af92 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ViewHolder.meta b/Runtime/RecyclerView/ViewHolder.meta new file mode 100644 index 0000000..a317212 --- /dev/null +++ b/Runtime/RecyclerView/ViewHolder.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 194b0a1eb83729844850e1873dbc0118 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ViewHolder/Example.meta b/Runtime/RecyclerView/ViewHolder/Example.meta new file mode 100644 index 0000000..2e74224 --- /dev/null +++ b/Runtime/RecyclerView/ViewHolder/Example.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 72a222ebb6ae56346b65b78fa3d60143 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ViewHolder/Example/SimpleViewHolder.cs b/Runtime/RecyclerView/ViewHolder/Example/SimpleViewHolder.cs new file mode 100644 index 0000000..2c0a661 --- /dev/null +++ b/Runtime/RecyclerView/ViewHolder/Example/SimpleViewHolder.cs @@ -0,0 +1,19 @@ +using AlicizaX.UI.RecyclerView; +using TMPro; + +public sealed class SimpleViewHolder : ViewHolder +{ + private TMP_Text simpleText; + + public override void FindUI() + { + simpleText = transform.Find("SimpleText").GetComponent(); + } + + public override void BindViewData(T data) + { + string text = data as string; + + simpleText.text = text; + } +} diff --git a/Runtime/RecyclerView/ViewHolder/Example/SimpleViewHolder.cs.meta b/Runtime/RecyclerView/ViewHolder/Example/SimpleViewHolder.cs.meta new file mode 100644 index 0000000..3c735fa --- /dev/null +++ b/Runtime/RecyclerView/ViewHolder/Example/SimpleViewHolder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 846ae5a2cd8b619459ecbadfc91e58f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ViewHolder/ViewHolder.cs b/Runtime/RecyclerView/ViewHolder/ViewHolder.cs new file mode 100644 index 0000000..3eedc7b --- /dev/null +++ b/Runtime/RecyclerView/ViewHolder/ViewHolder.cs @@ -0,0 +1,59 @@ +using System; +using UnityEngine; +using UnityEngine.UI; + +namespace AlicizaX.UI.RecyclerView +{ + public abstract class ViewHolder : MonoBehaviour + { + private bool isStarted; + + private RectTransform rectTransform; + public RectTransform RectTransform + { + get + { + if (rectTransform == null) + { + rectTransform = GetComponent(); + } + return rectTransform; + } + private set + { + rectTransform = value; + } + } + + public string Name { get; set; } + public int Index { get; set; } + + public Vector2 SizeDelta => RectTransform.sizeDelta; + + public virtual void OnStart() + { + if (!isStarted) + { + isStarted = true; + FindUI(); + } + } + + public virtual void OnStop() { } + + public abstract void FindUI(); + + public abstract void BindViewData(T data); + + public virtual void BindItemClick(T data, Action action) + { + if (TryGetComponent(out Button button)) + { + button.onClick.RemoveAllListeners(); + button.onClick.AddListener(() => action?.Invoke(data)); + } + } + + public virtual void BindChoiceState(bool state) { } + } +} diff --git a/Runtime/RecyclerView/ViewHolder/ViewHolder.cs.meta b/Runtime/RecyclerView/ViewHolder/ViewHolder.cs.meta new file mode 100644 index 0000000..c30a8e4 --- /dev/null +++ b/Runtime/RecyclerView/ViewHolder/ViewHolder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d43d46f3524d814fbb2fb2f03e3c610 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ViewProvider.meta b/Runtime/RecyclerView/ViewProvider.meta new file mode 100644 index 0000000..e4f683f --- /dev/null +++ b/Runtime/RecyclerView/ViewProvider.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 789e3dbb330a44f42b73d50f128888e4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs b/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs new file mode 100644 index 0000000..a4d64c4 --- /dev/null +++ b/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + public class MixedViewProvider : ViewProvider + { + [SerializeField] private ViewHolder chatLeftViewHolder; + [SerializeField] private ViewHolder chatRightViewHolder; + + private IMixedObjectPool objectPool; + private Dictionary dict = new(); + + public MixedViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates) + { + foreach (var template in templates) + { + dict[template.name] = template; + } + + UnityMixedComponentFactory factory = new(dict, recyclerView.Content); + objectPool = new MixedObjectPool(factory); + } + + public override ViewHolder GetTemplate(string viewName) + { + if (templates == null || templates.Length == 0) + { + throw new NullReferenceException("ViewProvider templates can not null or empty."); + } + return dict[viewName]; + } + + public override ViewHolder[] GetTemplates() + { + if (templates == null || templates.Length == 0) + { + throw new NullReferenceException("ViewProvider templates can not null or empty."); + } + return dict.Values.ToArray(); + } + + public override ViewHolder Allocate(string viewName) + { + var viewHolder = objectPool.Allocate(viewName); + viewHolder.gameObject.SetActive(true); + return viewHolder; + } + + public override void Free(string viewName, ViewHolder viewHolder) + { + objectPool.Free(viewName, viewHolder); + } + + public override void Reset() + { + Clear(); + objectPool.Dispose(); + } + } +} diff --git a/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs.meta b/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs.meta new file mode 100644 index 0000000..ff68aa5 --- /dev/null +++ b/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ef7ee3380d8e82c4f832f1e2535fe795 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs b/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs new file mode 100644 index 0000000..d17bc6d --- /dev/null +++ b/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs @@ -0,0 +1,51 @@ +using System; + +namespace AlicizaX.UI.RecyclerView +{ + public sealed class SimpleViewProvider : ViewProvider + { + private readonly IObjectPool objectPool; + + public SimpleViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates) + { + UnityComponentFactory factory = new(GetTemplate(), recyclerView.Content); + objectPool = new ObjectPool(factory, 100); + } + + public override ViewHolder GetTemplate(string viewName = "") + { + if (templates == null || templates.Length == 0) + { + throw new NullReferenceException("ViewProvider templates can not null or empty."); + } + return templates[0]; + } + + public override ViewHolder[] GetTemplates() + { + if (templates == null || templates.Length == 0) + { + throw new NullReferenceException("ViewProvider templates can not null or empty."); + } + return templates; + } + + public override ViewHolder Allocate(string viewName) + { + var viewHolder = objectPool.Allocate(); + viewHolder.gameObject.SetActive(true); + return viewHolder; + } + + public override void Free(string viewName, ViewHolder viewHolder) + { + objectPool.Free(viewHolder); + } + + public override void Reset() + { + Clear(); + objectPool.Dispose(); + } + } +} diff --git a/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs.meta b/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs.meta new file mode 100644 index 0000000..0b5d8a4 --- /dev/null +++ b/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6100883160a6b841a1d7084de2dcd76 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/RecyclerView/ViewProvider/ViewProvider.cs b/Runtime/RecyclerView/ViewProvider/ViewProvider.cs new file mode 100644 index 0000000..85969a6 --- /dev/null +++ b/Runtime/RecyclerView/ViewProvider/ViewProvider.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AlicizaX.UI.RecyclerView +{ + /// + /// 提供和管理 ViewHolder + /// + public abstract class ViewProvider + { + private readonly List viewHolders = new(); + + public IAdapter Adapter { get; set; } + public LayoutManager LayoutManager { get; set; } + + public List ViewHolders => viewHolders; + + protected RecyclerView recyclerView; + protected ViewHolder[] templates; + + public ViewProvider(RecyclerView recyclerView, ViewHolder[] templates) + { + this.recyclerView = recyclerView; + this.templates = templates; + } + + public abstract ViewHolder GetTemplate(string viewName); + + public abstract ViewHolder[] GetTemplates(); + + public abstract ViewHolder Allocate(string viewName); + + public abstract void Free(string viewName, ViewHolder viewHolder); + + public abstract void Reset(); + + public void CreateViewHolder(int index) + { + for (int i = index; i < index + LayoutManager.Unit; i++) + { + if (i > Adapter.GetItemCount() - 1) break; + + string viewName = Adapter.GetViewName(i); + var viewHolder = Allocate(viewName); + viewHolder.OnStart(); + viewHolder.Name = viewName; + viewHolder.Index = i; + viewHolders.Add(viewHolder); + + LayoutManager.Layout(viewHolder, i); + Adapter.OnBindViewHolder(viewHolder, i); + } + } + + public void RemoveViewHolder(int index) + { + for (int i = index; i < index + LayoutManager.Unit; i++) + { + if (i > Adapter.GetItemCount() - 1) break; + + int viewHolderIndex = GetViewHolderIndex(i); + + if (viewHolderIndex < 0 || viewHolderIndex >= viewHolders.Count) return; + + var viewHolder = viewHolders[viewHolderIndex]; + viewHolders.RemoveAt(viewHolderIndex); + viewHolder.OnStop(); + Free(viewHolder.Name, viewHolder); + } + } + + /// + /// 根据数据的下标获取对应的 ViewHolder + /// + /// 数据的下标 + /// + public ViewHolder GetViewHolder(int index) + { + foreach (var viewHolder in viewHolders) + { + if (viewHolder.Index == index) + { + return viewHolder; + } + } + return null; + } + + /// + /// 根据数据的下标获取 ViewHolder 的下标 + /// + /// 数据的下标 + /// + public int GetViewHolderIndex(int index) + { + for (int i = 0; i < viewHolders.Count; i++) + { + if (viewHolders[i].Index == index) + { + return i; + } + } + return -1; + } + + public void Clear() + { + foreach (var viewHolder in viewHolders) + { + Free(viewHolder.Name, viewHolder); + } + viewHolders.Clear(); + } + + /// + /// 计算 ViewHolder 的尺寸 + /// + /// + /// + public Vector2 CalculateViewSize(int index) + { + Vector2 size = GetTemplate(Adapter.GetViewName(index)).SizeDelta; + return size; + } + + public int GetItemCount() + { + return Adapter == null ? 0 : Adapter.GetItemCount(); + } + } +} diff --git a/Runtime/RecyclerView/ViewProvider/ViewProvider.cs.meta b/Runtime/RecyclerView/ViewProvider/ViewProvider.cs.meta new file mode 100644 index 0000000..2def4f4 --- /dev/null +++ b/Runtime/RecyclerView/ViewProvider/ViewProvider.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b1c2cd7d2729fb4488ffd0e25e0fbf43 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: