From f22a4f1efa16de9306f02065016ced084da9ab24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Fri, 27 Mar 2026 18:38:29 +0800 Subject: [PATCH] =?UTF-8?q?RecyclerView=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.分离逻辑视图与渲染视图 2.xxxx若干项懒得写了 --- Runtime/RecyclerView/Adapter/Adapter.cs | 407 +++++++++++------- Runtime/RecyclerView/Adapter/GroupAdapter.cs | 56 +-- Runtime/RecyclerView/Adapter/IAdapter.cs | 2 + Runtime/RecyclerView/Adapter/ItemRender.cs | 203 +++++++++ .../RecyclerView/Adapter/ItemRender.cs.meta | 11 + Runtime/RecyclerView/Adapter/LoopAdapter.cs | 5 - Runtime/RecyclerView/Adapter/MixedAdapter.cs | 5 - Runtime/RecyclerView/UGList.cs | 140 ++---- .../ViewHolder/InteractiveViewHolder.cs | 22 + .../ViewHolder/InteractiveViewHolder.cs.meta | 11 + Runtime/RecyclerView/ViewHolder/ViewHolder.cs | 97 ++--- .../ViewProvider/MixedViewProvider.cs | 1 + .../ViewProvider/SimpleViewProvider.cs | 1 + .../RecyclerView/ViewProvider/ViewProvider.cs | 3 + 14 files changed, 615 insertions(+), 349 deletions(-) create mode 100644 Runtime/RecyclerView/Adapter/ItemRender.cs create mode 100644 Runtime/RecyclerView/Adapter/ItemRender.cs.meta create mode 100644 Runtime/RecyclerView/ViewHolder/InteractiveViewHolder.cs create mode 100644 Runtime/RecyclerView/ViewHolder/InteractiveViewHolder.cs.meta diff --git a/Runtime/RecyclerView/Adapter/Adapter.cs b/Runtime/RecyclerView/Adapter/Adapter.cs index 71aad78..78cef3a 100644 --- a/Runtime/RecyclerView/Adapter/Adapter.cs +++ b/Runtime/RecyclerView/Adapter/Adapter.cs @@ -3,125 +3,160 @@ using System.Collections.Generic; namespace AlicizaX.UI { - /// - /// RecyclerView 的通用适配器,支持数据绑定、选中状态和列表操作 - /// - /// 数据类型,必须实现 ISimpleViewData 接口 - public class Adapter : IAdapter where T : ISimpleViewData + internal interface IItemRenderCacheOwner { - protected RecyclerView recyclerView; + void ReleaseAllItemRenders(); + } + public class Adapter : IAdapter, IItemRenderCacheOwner where T : ISimpleViewData + { + private sealed class ItemRenderEntry + { + public ItemRenderEntry(string viewName, IItemRender itemRender) + { + ViewName = viewName; + ItemRender = itemRender; + } + + public string ViewName { get; } + + public IItemRender ItemRender { get; } + } + + protected RecyclerView recyclerView; protected List list; - protected Action onItemClick; protected int choiceIndex = -1; + private readonly Dictionary itemRenderDefinitions = new(StringComparer.Ordinal); + private readonly Dictionary itemRenders = new(); + private ItemRenderResolver.ItemRenderDefinition defaultItemRenderDefinition; - /// - /// 当前选中项的索引 - /// public int ChoiceIndex { get => choiceIndex; - set { SetChoiceIndex(value); } + set => SetChoiceIndex(value); } - /// - /// 构造函数 - /// - /// RecyclerView 实例 - public Adapter(RecyclerView recyclerView) : this(recyclerView, new List(), null) + public Adapter(RecyclerView recyclerView) : this(recyclerView, new List()) { } - /// - /// 构造函数 - /// - /// RecyclerView 实例 - /// 数据列表 - public Adapter(RecyclerView recyclerView, List list) : this(recyclerView, list, null) - { - } - - /// - /// 构造函数 - /// - /// RecyclerView 实例 - /// 数据列表 - /// 列表项点击回调 - public Adapter(RecyclerView recyclerView, List list, Action onItemClick) + public Adapter(RecyclerView recyclerView, List list) { this.recyclerView = recyclerView; this.list = list; - this.onItemClick = onItemClick; } - /// - /// 获取列表项总数 - /// 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; + if (viewHolder == null) return; + if (!TryGetBindData(index, out var data)) return; - T data = list[index]; - - viewHolder.BindViewData(data); - viewHolder.BindItemClick(data, t => + string viewName = GetViewName(index); + Action defaultClickAction = CreateItemClickAction(index, data); + if (TryGetOrCreateItemRender(viewHolder, viewName, out var itemRender)) { - SetChoiceIndex(index); - onItemClick?.Invoke(data); - }); - viewHolder.BindChoiceState(index == choiceIndex); + itemRender.Bind(data, index, defaultClickAction); + itemRender.UpdateSelection(index == choiceIndex); + return; + } + + string resolvedViewName = string.IsNullOrEmpty(viewName) ? "" : viewName; + throw new InvalidOperationException( + $"RecyclerView item render is missing for view '{resolvedViewName}'. Holder='{viewHolder.GetType().Name}', Adapter='{GetType().Name}'."); + } + + public virtual void OnRecycleViewHolder(ViewHolder viewHolder) + { + if (viewHolder == null) return; + + if (TryGetItemRender(viewHolder, out var itemRender)) + { + itemRender.UpdateSelection(false); + itemRender.Unbind(); + } + + viewHolder.ClearInteractionCallbacks(); } - /// - /// 通知数据已更改 - /// public virtual void NotifyDataChanged() { recyclerView.RequestLayout(); recyclerView.Refresh(); } - /// - /// 设置数据列表并刷新视图 - /// - /// 新的数据列表 - public virtual void SetList(List list) + public virtual void SetList(List list) { this.list = list; recyclerView.Reset(); NotifyDataChanged(); } - /// - /// 获取指定索引的数据 - /// - /// 数据索引 - /// 数据对象,索引无效时返回 default + public void RegisterItemRender(string viewName = "") where TItemRender : IItemRender + { + RegisterItemRender(typeof(TItemRender), viewName); + } + + public void RegisterItemRender(Type itemRenderType, string viewName = "") + { + var definition = ItemRenderResolver.GetOrCreate(itemRenderType); + + if (string.IsNullOrEmpty(viewName)) + { + ReleaseCachedItemRenders(string.Empty); + defaultItemRenderDefinition = definition; + return; + } + + ReleaseCachedItemRenders(viewName); + itemRenderDefinitions[viewName] = definition; + } + + public bool UnregisterItemRender(string viewName = "") + { + if (string.IsNullOrEmpty(viewName)) + { + bool hadDefault = defaultItemRenderDefinition != null; + defaultItemRenderDefinition = null; + if (hadDefault) + { + ReleaseCachedItemRenders(string.Empty); + } + + return hadDefault; + } + + bool removed = itemRenderDefinitions.Remove(viewName); + if (removed) + { + ReleaseCachedItemRenders(viewName); + } + + return removed; + } + + public void ClearItemRenderRegistrations() + { + ReleaseAllItemRenders(); + itemRenderDefinitions.Clear(); + defaultItemRenderDefinition = null; + } + public T GetData(int index) { if (index < 0 || index >= GetItemCount()) return default; @@ -129,62 +164,36 @@ namespace AlicizaX.UI 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; @@ -193,112 +202,218 @@ namespace AlicizaX.UI 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 (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var oldHolder)) { - if (TryGetViewHolder(choiceIndex, out var viewHolder)) - { - viewHolder.BindChoiceState(false); - } + UpdateSelectionState(oldHolder, false); } choiceIndex = index; - if (choiceIndex != -1) + if (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var newHolder)) { - if (TryGetViewHolder(choiceIndex, out var viewHolder)) - { - viewHolder.BindChoiceState(true); - } + UpdateSelectionState(newHolder, true); } } - /// - /// 尝试获取指定索引的视图持有者 - /// - /// 数据索引 - /// 输出的视图持有者 - /// 是否成功获取 private bool TryGetViewHolder(int index, out ViewHolder viewHolder) { viewHolder = recyclerView.ViewProvider.GetViewHolder(index); return viewHolder != null; } + + protected virtual bool TryGetBindData(int index, out T data) + { + if (list == null || index < 0 || index >= list.Count) + { + data = default; + return false; + } + + data = list[index]; + return true; + } + + protected virtual Action CreateItemClickAction(int index, T data) + { + return () => { SetChoiceIndex(index); }; + } + + private bool TryGetItemRenderDefinition(string viewName, out ItemRenderResolver.ItemRenderDefinition definition) + { + if (!string.IsNullOrEmpty(viewName) && itemRenderDefinitions.TryGetValue(viewName, out definition)) + { + return true; + } + + definition = defaultItemRenderDefinition; + return definition != null; + } + + private void UpdateSelectionState(ViewHolder viewHolder, bool selected) + { + if (viewHolder == null) + { + return; + } + + if (TryGetItemRender(viewHolder, out var itemRender)) + { + itemRender.UpdateSelection(selected); + return; + } + + string viewName = string.IsNullOrEmpty(viewHolder.Name) ? "" : viewHolder.Name; + throw new InvalidOperationException( + $"RecyclerView item render is missing for selection update. View='{viewName}', Holder='{viewHolder.GetType().Name}', Adapter='{GetType().Name}'."); + } + + private bool TryGetItemRender(ViewHolder viewHolder, out IItemRender itemRender) + { + if (viewHolder != null && + itemRenders.TryGetValue(viewHolder, out var entry) && + entry.ItemRender != null) + { + itemRender = entry.ItemRender; + return true; + } + + itemRender = null; + return false; + } + + private bool TryGetOrCreateItemRender(ViewHolder viewHolder, string viewName, out IItemRender itemRender) + { + if (viewHolder == null) + { + itemRender = null; + return false; + } + + if (itemRenders.TryGetValue(viewHolder, out var entry)) + { + if (entry.ItemRender != null && string.Equals(entry.ViewName, viewName, StringComparison.Ordinal)) + { + itemRender = entry.ItemRender; + return true; + } + + entry.ItemRender?.Unbind(); + viewHolder.Destroyed -= OnViewHolderDestroyed; + itemRenders.Remove(viewHolder); + } + + if (!TryGetItemRenderDefinition(viewName, out var definition)) + { + itemRender = null; + return false; + } + + itemRender = definition.Create(viewHolder); + itemRenders[viewHolder] = new ItemRenderEntry(viewName, itemRender); + viewHolder.Destroyed += OnViewHolderDestroyed; + return true; + } + + void IItemRenderCacheOwner.ReleaseAllItemRenders() + { + ReleaseAllItemRenders(); + } + + private void ReleaseAllItemRenders() + { + foreach (var pair in itemRenders) + { + pair.Value.ItemRender?.Unbind(); + if (pair.Key != null) + { + pair.Key.Destroyed -= OnViewHolderDestroyed; + } + } + + itemRenders.Clear(); + } + + private void ReleaseCachedItemRenders(string viewName) + { + if (itemRenders.Count == 0) + { + return; + } + + List viewHoldersToRemove = null; + foreach (var pair in itemRenders) + { + if (!string.Equals(pair.Value.ViewName, viewName, StringComparison.Ordinal)) + { + continue; + } + + pair.Value.ItemRender?.Unbind(); + pair.Key.Destroyed -= OnViewHolderDestroyed; + viewHoldersToRemove ??= new List(); + viewHoldersToRemove.Add(pair.Key); + } + + if (viewHoldersToRemove == null) + { + return; + } + + for (int i = 0; i < viewHoldersToRemove.Count; i++) + { + itemRenders.Remove(viewHoldersToRemove[i]); + } + } + + private void OnViewHolderDestroyed(ViewHolder viewHolder) + { + if (viewHolder == null) + { + return; + } + + viewHolder.Destroyed -= OnViewHolderDestroyed; + itemRenders.Remove(viewHolder); + } } } diff --git a/Runtime/RecyclerView/Adapter/GroupAdapter.cs b/Runtime/RecyclerView/Adapter/GroupAdapter.cs index 1bd2740..736bd7d 100644 --- a/Runtime/RecyclerView/Adapter/GroupAdapter.cs +++ b/Runtime/RecyclerView/Adapter/GroupAdapter.cs @@ -6,7 +6,7 @@ namespace AlicizaX.UI public class GroupAdapter : Adapter where TData : IGroupViewData, new() { private readonly List showList = new(); - private string groupViewName; + private readonly string groupViewName; public GroupAdapter(RecyclerView recyclerView, string groupViewName) : base(recyclerView) { @@ -17,15 +17,10 @@ namespace AlicizaX.UI { } - public GroupAdapter(RecyclerView recyclerView, List list, Action onItemClick) : base(recyclerView, list, onItemClick) - { - } - public GroupAdapter(RecyclerView recyclerView) : base(recyclerView) { } - public override int GetItemCount() { return showList.Count; @@ -36,28 +31,6 @@ namespace AlicizaX.UI return showList[index].TemplateName; } - public override void OnBindViewHolder(ViewHolder viewHolder, int index) - { - if (index < 0 || index >= GetItemCount()) return; - - TData data = showList[index]; - - viewHolder.BindViewData(data); - viewHolder.BindItemClick(data, t => - { - if (data.TemplateName == groupViewName) - { - data.Expanded = !data.Expanded; - NotifyDataChanged(); - } - else - { - SetChoiceIndex(index); - onItemClick?.Invoke(data); - } - }); - } - public override void NotifyDataChanged() { foreach (var data in list) @@ -118,5 +91,32 @@ namespace AlicizaX.UI var collapseList = showList.FindAll(data => data.Type == showList[index].Type && data.TemplateName != groupViewName); showList.RemoveRange(index + 1, collapseList.Count); } + + protected override bool TryGetBindData(int index, out TData data) + { + if (index < 0 || index >= showList.Count) + { + data = default; + return false; + } + + data = showList[index]; + return true; + } + + protected override Action CreateItemClickAction(int index, TData data) + { + return () => + { + if (data.TemplateName == groupViewName) + { + data.Expanded = !data.Expanded; + NotifyDataChanged(); + return; + } + + SetChoiceIndex(index); + }; + } } } diff --git a/Runtime/RecyclerView/Adapter/IAdapter.cs b/Runtime/RecyclerView/Adapter/IAdapter.cs index dcfcb88..df32630 100644 --- a/Runtime/RecyclerView/Adapter/IAdapter.cs +++ b/Runtime/RecyclerView/Adapter/IAdapter.cs @@ -31,6 +31,8 @@ namespace AlicizaX.UI /// 数据索引 void OnBindViewHolder(ViewHolder viewHolder, int index); + void OnRecycleViewHolder(ViewHolder viewHolder); + /// /// 通知数据已更改,触发视图刷新 /// diff --git a/Runtime/RecyclerView/Adapter/ItemRender.cs b/Runtime/RecyclerView/Adapter/ItemRender.cs new file mode 100644 index 0000000..cb51b97 --- /dev/null +++ b/Runtime/RecyclerView/Adapter/ItemRender.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AlicizaX.UI +{ + public interface IItemRender + { + void Bind(object data, int index, Action defaultClickAction); + + void UpdateSelection(bool selected); + + void Unbind(); + } + + public abstract class ItemRender : IItemRender + where THolder : ViewHolder + { + private Action defaultClickAction; + + protected ItemRender(THolder holder) + { + Holder = holder ?? throw new ArgumentNullException(nameof(holder)); + CurrentIndex = -1; + } + + protected THolder Holder { get; } + + protected TData CurrentData { get; private set; } + + protected int CurrentIndex { get; private set; } + + public void Bind(object data, int index, Action defaultClickAction) + { + if (data is not TData itemData) + { + throw new InvalidCastException( + $"ItemRender '{GetType().Name}' expected data '{typeof(TData).Name}', but got '{data?.GetType().Name ?? "null"}'."); + } + + CurrentData = itemData; + CurrentIndex = index; + this.defaultClickAction = defaultClickAction; + Holder.SetInteractionCallbacks(HandleClick, HandlePointerEnter, HandlePointerExit); + Bind(itemData, index); + } + + public void UpdateSelection(bool selected) + { + OnSelectionChanged(selected); + } + + public void Unbind() + { + OnClear(); + CurrentData = default; + CurrentIndex = -1; + defaultClickAction = null; + Holder.ClearInteractionCallbacks(); + } + + protected abstract void Bind(TData data, int index); + + protected virtual void OnSelectionChanged(bool selected) + { + } + + protected virtual void OnClear() + { + } + + protected virtual void OnClick() + { + } + + protected virtual void OnPointerEnter() + { + } + + protected virtual void OnPointerExit() + { + } + + private void HandleClick() + { + defaultClickAction?.Invoke(); + OnClick(); + } + + private void HandlePointerEnter() + { + OnPointerEnter(); + } + + private void HandlePointerExit() + { + OnPointerExit(); + } + } + + internal static class ItemRenderResolver + { + internal sealed class ItemRenderDefinition + { + private readonly ConstructorInfo constructor; + + public ItemRenderDefinition(Type itemRenderType, Type holderType, ConstructorInfo constructor) + { + ItemRenderType = itemRenderType; + HolderType = holderType; + this.constructor = constructor; + } + + public Type ItemRenderType { get; } + + public Type HolderType { get; } + + public IItemRender Create(ViewHolder viewHolder) + { + if (viewHolder == null) + { + throw new ArgumentNullException(nameof(viewHolder)); + } + + if (!HolderType.IsInstanceOfType(viewHolder)) + { + throw new InvalidOperationException( + $"RecyclerView item render '{ItemRenderType.FullName}' expects holder '{HolderType.FullName}', but got '{viewHolder.GetType().FullName}'."); + } + + return (IItemRender)constructor.Invoke(new object[] { viewHolder }); + } + } + + private static readonly Dictionary Definitions = new(); + + public static ItemRenderDefinition GetOrCreate(Type itemRenderType) + { + if (itemRenderType == null) + { + throw new ArgumentNullException(nameof(itemRenderType)); + } + + if (Definitions.TryGetValue(itemRenderType, out ItemRenderDefinition definition)) + { + return definition; + } + + definition = CreateDefinition(itemRenderType); + Definitions[itemRenderType] = definition; + return definition; + } + + private static ItemRenderDefinition CreateDefinition(Type itemRenderType) + { + if (itemRenderType.IsAbstract || + itemRenderType.IsInterface || + itemRenderType.ContainsGenericParameters || + !typeof(IItemRender).IsAssignableFrom(itemRenderType)) + { + throw new InvalidOperationException( + $"RecyclerView item render type '{itemRenderType.FullName}' is invalid."); + } + + if (!TryGetHolderType(itemRenderType, out Type holderType)) + { + throw new InvalidOperationException( + $"RecyclerView item render '{itemRenderType.FullName}' must inherit from ItemRender."); + } + + ConstructorInfo constructor = itemRenderType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + null, + new[] { holderType }, + null); + + if (constructor == null) + { + throw new InvalidOperationException( + $"RecyclerView item render '{itemRenderType.FullName}' must declare a constructor with a single '{holderType.FullName}' parameter."); + } + + return new ItemRenderDefinition(itemRenderType, holderType, constructor); + } + + private static bool TryGetHolderType(Type itemRenderType, out Type holderType) + { + for (Type current = itemRenderType; current != null && current != typeof(object); current = current.BaseType) + { + if (current.IsGenericType && + current.GetGenericTypeDefinition() == typeof(ItemRender<,>)) + { + Type[] arguments = current.GetGenericArguments(); + holderType = arguments[1]; + return true; + } + } + + holderType = null; + return false; + } + } +} diff --git a/Runtime/RecyclerView/Adapter/ItemRender.cs.meta b/Runtime/RecyclerView/Adapter/ItemRender.cs.meta new file mode 100644 index 0000000..e08c420 --- /dev/null +++ b/Runtime/RecyclerView/Adapter/ItemRender.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5baf3050ddf617e4aa583b8ec23f82ce +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 index d1f2c15..69182bf 100644 --- a/Runtime/RecyclerView/Adapter/LoopAdapter.cs +++ b/Runtime/RecyclerView/Adapter/LoopAdapter.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; namespace AlicizaX.UI @@ -13,10 +12,6 @@ namespace AlicizaX.UI { } - public LoopAdapter(RecyclerView recyclerView, List list, Action onItemClick) : base(recyclerView, list, onItemClick) - { - } - public override int GetItemCount() { return int.MaxValue; diff --git a/Runtime/RecyclerView/Adapter/MixedAdapter.cs b/Runtime/RecyclerView/Adapter/MixedAdapter.cs index 437d1f6..d156e48 100644 --- a/Runtime/RecyclerView/Adapter/MixedAdapter.cs +++ b/Runtime/RecyclerView/Adapter/MixedAdapter.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; namespace AlicizaX.UI @@ -13,10 +12,6 @@ namespace AlicizaX.UI { } - public MixedAdapter(RecyclerView recyclerView, List list, Action onItemClick) : base(recyclerView, list, onItemClick) - { - } - public override string GetViewName(int index) { return list[index].TemplateName; diff --git a/Runtime/RecyclerView/UGList.cs b/Runtime/RecyclerView/UGList.cs index a9da453..2e9a186 100644 --- a/Runtime/RecyclerView/UGList.cs +++ b/Runtime/RecyclerView/UGList.cs @@ -3,30 +3,14 @@ using System.Collections.Generic; namespace AlicizaX.UI { - /// - /// UGList 基类 - /// 提供简化的列表操作接口,封装 RecyclerView 和 Adapter 的交互 - /// - /// 数据类型 - /// 适配器类型 public abstract class UGListBase where TAdapter : Adapter where TData : ISimpleViewData { protected readonly RecyclerView _recyclerView; - protected readonly TAdapter _adapter; - /// - /// 获取关联的 RecyclerView 实例 - /// public RecyclerView RecyclerView => _recyclerView; - /// - /// 构造函数 - /// - /// RecyclerView 实例 - /// 适配器实例 - /// 列表项点击回调 - public UGListBase(RecyclerView recyclerView, TAdapter adapter, Action onItemClick = null) + public UGListBase(RecyclerView recyclerView, TAdapter adapter) { _recyclerView = recyclerView; _adapter = adapter; @@ -35,24 +19,32 @@ namespace AlicizaX.UI { _recyclerView.SetAdapter(_adapter); } - - if (onItemClick != null) - { - _adapter.SetOnItemClick(onItemClick); - } } - /// - /// 获取适配器实例 - /// public TAdapter Adapter => _adapter; + public void RegisterItemRender(string viewName = "") where TItemRender : IItemRender + { + _adapter.RegisterItemRender(viewName); + } + + public void RegisterItemRender(Type itemRenderType, string viewName = "") + { + _adapter.RegisterItemRender(itemRenderType, viewName); + } + + public bool UnregisterItemRender(string viewName = "") + { + return _adapter.UnregisterItemRender(viewName); + } + + public void ClearItemRenderRegistrations() + { + _adapter.ClearItemRenderRegistrations(); + } + private List _datas; - /// - /// 获取或设置数据列表 - /// 设置时会自动更新适配器 - /// public List Data { get => _datas; @@ -64,108 +56,50 @@ namespace AlicizaX.UI } } - /// - /// 通用列表类 - /// 用于显示简单的单一类型数据列表 - /// - /// 数据类型,必须实现 ISimpleViewData public class UGList : UGListBase> where TData : ISimpleViewData { - /// - /// 构造函数 - /// - /// RecyclerView 实例 - /// 列表项点击回调 - public UGList(RecyclerView recyclerView, Action onItemClick = null) - : base(recyclerView, new Adapter(recyclerView), onItemClick) + public UGList(RecyclerView recyclerView) + : base(recyclerView, new Adapter(recyclerView)) { } } - /// - /// 分组列表类 - /// 用于显示带有分组头的列表数据 - /// - /// 数据类型,必须实现 IGroupViewData public class UGGroupList : UGListBase> where TData : class, IGroupViewData, new() { - /// - /// 构造函数 - /// - /// RecyclerView 实例 - /// 分组头视图名称 - /// 列表项点击回调 - public UGGroupList(RecyclerView recyclerView, string groupViewName, Action onItemClick = null) - : base(recyclerView, new GroupAdapter(recyclerView, groupViewName), onItemClick) + public UGGroupList(RecyclerView recyclerView, string groupViewName) + : base(recyclerView, new GroupAdapter(recyclerView, groupViewName)) { } } - /// - /// 循环列表类 - /// 用于实现无限循环滚动的列表 - /// - /// 数据类型,必须实现 ISimpleViewData public class UGLoopList : UGListBase> where TData : ISimpleViewData, new() { - /// - /// 构造函数 - /// - /// RecyclerView 实例 - /// 列表项点击回调 - public UGLoopList(RecyclerView recyclerView, Action onItemClick = null) - : base(recyclerView, new LoopAdapter(recyclerView), onItemClick) + public UGLoopList(RecyclerView recyclerView) + : base(recyclerView, new LoopAdapter(recyclerView)) { } } - /// - /// 混合列表类 - /// 用于显示多种不同类型的列表项 - /// - /// 数据类型,必须实现 IMixedViewData public class UGMixedList : UGListBase> where TData : IMixedViewData { - /// - /// 构造函数 - /// - /// RecyclerView 实例 - /// 列表项点击回调 - public UGMixedList(RecyclerView recyclerView, Action onItemClick = null) - : base(recyclerView, new MixedAdapter(recyclerView), onItemClick) + public UGMixedList(RecyclerView recyclerView) + : base(recyclerView, new MixedAdapter(recyclerView)) { } } - /// - /// UGList 创建辅助类 - /// 提供便捷的静态方法来创建各种类型的列表 - /// public static class UGListCreateHelper { - /// - /// 创建通用列表 - /// - public static UGList Create(RecyclerView recyclerView, Action onItemClick = null) where TData : ISimpleViewData - => new UGList(recyclerView, onItemClick); + public static UGList Create(RecyclerView recyclerView) where TData : ISimpleViewData + => new UGList(recyclerView); - /// - /// 创建分组列表 - /// - public static UGGroupList CreateGroup(RecyclerView recyclerView, string groupViewName, Action onItemClick = null) where TData : class, IGroupViewData, new() - => new UGGroupList(recyclerView, groupViewName, onItemClick); + public static UGGroupList CreateGroup(RecyclerView recyclerView, string groupViewName) where TData : class, IGroupViewData, new() + => new UGGroupList(recyclerView, groupViewName); - /// - /// 创建循环列表 - /// - public static UGLoopList CreateLoop(RecyclerView recyclerView, Action onItemClick = null) where TData : ISimpleViewData, new() - => new UGLoopList(recyclerView, onItemClick); + public static UGLoopList CreateLoop(RecyclerView recyclerView) where TData : ISimpleViewData, new() + => new UGLoopList(recyclerView); - /// - /// 创建混合列表 - /// - public static UGMixedList CreateMixed(RecyclerView recyclerView, Action onItemClick = null) where TData : IMixedViewData - => new UGMixedList(recyclerView, onItemClick); + public static UGMixedList CreateMixed(RecyclerView recyclerView) where TData : IMixedViewData + => new UGMixedList(recyclerView); } - } diff --git a/Runtime/RecyclerView/ViewHolder/InteractiveViewHolder.cs b/Runtime/RecyclerView/ViewHolder/InteractiveViewHolder.cs new file mode 100644 index 0000000..d6de0d4 --- /dev/null +++ b/Runtime/RecyclerView/ViewHolder/InteractiveViewHolder.cs @@ -0,0 +1,22 @@ +using UnityEngine.EventSystems; + +namespace AlicizaX.UI +{ + public abstract class InteractiveViewHolder : ViewHolder, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler + { + public virtual void OnPointerClick(PointerEventData eventData) + { + InvokeClickAction(); + } + + public virtual void OnPointerEnter(PointerEventData eventData) + { + InvokePointerEnterAction(); + } + + public virtual void OnPointerExit(PointerEventData eventData) + { + InvokePointerExitAction(); + } + } +} diff --git a/Runtime/RecyclerView/ViewHolder/InteractiveViewHolder.cs.meta b/Runtime/RecyclerView/ViewHolder/InteractiveViewHolder.cs.meta new file mode 100644 index 0000000..d7b2804 --- /dev/null +++ b/Runtime/RecyclerView/ViewHolder/InteractiveViewHolder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 491a09d781095104989abb7a91424008 +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 index a0ee452..7d7c867 100644 --- a/Runtime/RecyclerView/ViewHolder/ViewHolder.cs +++ b/Runtime/RecyclerView/ViewHolder/ViewHolder.cs @@ -1,20 +1,17 @@ using System; -using AlicizaX.UI.Extension; using UnityEngine; -using UnityEngine.UI; namespace AlicizaX.UI { - /// - /// 视图持有者基类,用于缓存和复用列表项视图 - /// public abstract class ViewHolder : MonoBehaviour { private RectTransform rectTransform; + private Action clickAction; + private Action pointerEnterAction; + private Action pointerExitAction; + + internal event Action Destroyed; - /// - /// 获取 RectTransform 组件 - /// public RectTransform RectTransform { get @@ -26,83 +23,59 @@ namespace AlicizaX.UI return rectTransform; } - private set { rectTransform = value; } + private set => rectTransform = value; } - /// - /// 视图名称,用于区分不同类型的视图 - /// public string Name { get; internal set; } - /// - /// 当前绑定的数据索引 - /// public int Index { get; internal set; } - /// - /// 选中状态 - /// - public bool ChoiseState { private set; get; } - - /// - /// 获取视图的尺寸 - /// public Vector2 SizeDelta => RectTransform.sizeDelta; - - /// - /// 视图首次创建时调用 - /// protected internal virtual void OnStart() { } - /// - /// 视图被回收到对象池时调用 - /// protected internal virtual void OnRecycled() { } - /// - /// 绑定视图数据(抽象方法,子类必须实现) - /// - /// 数据类型 - /// 要绑定的数据 - public abstract void BindViewData(T data); - - /// - /// 绑定列表项点击事件 - /// - /// 数据类型 - /// 数据对象 - /// 点击回调 - protected internal virtual void BindItemClick(T data, Action action) + internal void SetInteractionCallbacks( + Action clickAction = null, + Action pointerEnterAction = null, + Action pointerExitAction = null) { - // _button.onClick.RemoveAllListeners(); - // _button.onClick.AddListener(() => action?.Invoke(data)); + this.clickAction = clickAction; + this.pointerEnterAction = pointerEnterAction; + this.pointerExitAction = pointerExitAction; } - - /// - /// 绑定选中状态 - /// - /// 是否选中 - protected internal void BindChoiceState(bool state) + public void ClearInteractionCallbacks() { - if (ChoiseState != state) - { - ChoiseState = state; - OnBindChoiceState(state); - } + clickAction = null; + pointerEnterAction = null; + pointerExitAction = null; } - /// - /// 选中状态改变时的回调(可在子类中重写) - /// - /// 是否选中 - protected internal virtual void OnBindChoiceState(bool state) + protected void InvokeClickAction() { + clickAction?.Invoke(); + } + + protected void InvokePointerEnterAction() + { + pointerEnterAction?.Invoke(); + } + + protected void InvokePointerExitAction() + { + pointerExitAction?.Invoke(); + } + + protected virtual void OnDestroy() + { + Destroyed?.Invoke(this); + Destroyed = null; } } } diff --git a/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs b/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs index 705f065..5024d93 100644 --- a/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs +++ b/Runtime/RecyclerView/ViewProvider/MixedViewProvider.cs @@ -57,6 +57,7 @@ namespace AlicizaX.UI public override void Reset() { Clear(); + (Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders(); objectPool.Dispose(); } } diff --git a/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs b/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs index 7811a29..62505ec 100644 --- a/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs +++ b/Runtime/RecyclerView/ViewProvider/SimpleViewProvider.cs @@ -45,6 +45,7 @@ namespace AlicizaX.UI public override void Reset() { Clear(); + (Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders(); objectPool.Dispose(); } } diff --git a/Runtime/RecyclerView/ViewProvider/ViewProvider.cs b/Runtime/RecyclerView/ViewProvider/ViewProvider.cs index 688ab56..934b425 100644 --- a/Runtime/RecyclerView/ViewProvider/ViewProvider.cs +++ b/Runtime/RecyclerView/ViewProvider/ViewProvider.cs @@ -111,6 +111,7 @@ namespace AlicizaX.UI var viewHolder = viewHolders[viewHolderIndex]; viewHolders.RemoveAt(viewHolderIndex); + Adapter?.OnRecycleViewHolder(viewHolder); viewHolder.OnRecycled(); Free(viewHolder.Name, viewHolder); } @@ -160,6 +161,8 @@ namespace AlicizaX.UI { foreach (var viewHolder in viewHolders) { + Adapter?.OnRecycleViewHolder(viewHolder); + viewHolder.OnRecycled(); Free(viewHolder.Name, viewHolder); }