RecyclerView优化

1.分离逻辑视图与渲染视图
2.xxxx若干项懒得写了
This commit is contained in:
陈思海 2026-03-27 18:38:29 +08:00
parent 91f7641f59
commit f22a4f1efa
14 changed files with 615 additions and 349 deletions

View File

@ -3,125 +3,160 @@ using System.Collections.Generic;
namespace AlicizaX.UI
{
/// <summary>
/// RecyclerView 的通用适配器,支持数据绑定、选中状态和列表操作
/// </summary>
/// <typeparam name="T">数据类型,必须实现 ISimpleViewData 接口</typeparam>
public class Adapter<T> : IAdapter where T : ISimpleViewData
internal interface IItemRenderCacheOwner
{
protected RecyclerView recyclerView;
void ReleaseAllItemRenders();
}
public class Adapter<T> : 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<T> list;
protected Action<T> onItemClick;
protected int choiceIndex = -1;
private readonly Dictionary<string, ItemRenderResolver.ItemRenderDefinition> itemRenderDefinitions = new(StringComparer.Ordinal);
private readonly Dictionary<ViewHolder, ItemRenderEntry> itemRenders = new();
private ItemRenderResolver.ItemRenderDefinition defaultItemRenderDefinition;
/// <summary>
/// 当前选中项的索引
/// </summary>
public int ChoiceIndex
{
get => choiceIndex;
set { SetChoiceIndex(value); }
set => SetChoiceIndex(value);
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">RecyclerView 实例</param>
public Adapter(RecyclerView recyclerView) : this(recyclerView, new List<T>(), null)
public Adapter(RecyclerView recyclerView) : this(recyclerView, new List<T>())
{
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">RecyclerView 实例</param>
/// <param name="list">数据列表</param>
public Adapter(RecyclerView recyclerView, List<T> list) : this(recyclerView, list, null)
{
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">RecyclerView 实例</param>
/// <param name="list">数据列表</param>
/// <param name="onItemClick">列表项点击回调</param>
public Adapter(RecyclerView recyclerView, List<T> list, Action<T> onItemClick)
public Adapter(RecyclerView recyclerView, List<T> list)
{
this.recyclerView = recyclerView;
this.list = list;
this.onItemClick = onItemClick;
}
/// <summary>
/// 获取列表项总数
/// </summary>
public virtual int GetItemCount()
{
return list == null ? 0 : list.Count;
}
/// <summary>
/// 获取实际数据项数量
/// </summary>
public virtual int GetRealCount()
{
return GetItemCount();
}
/// <summary>
/// 获取指定索引位置的视图名称
/// </summary>
public virtual string GetViewName(int index)
{
return "";
}
/// <summary>
/// 绑定视图持有者与数据
/// </summary>
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) ? "<default>" : 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();
}
/// <summary>
/// 通知数据已更改
/// </summary>
public virtual void NotifyDataChanged()
{
recyclerView.RequestLayout();
recyclerView.Refresh();
}
/// <summary>
/// 设置数据列表并刷新视图
/// </summary>
/// <param name="list">新的数据列表</param>
public virtual void SetList(List<T> list)
public virtual void SetList(List<T> list)
{
this.list = list;
recyclerView.Reset();
NotifyDataChanged();
}
/// <summary>
/// 获取指定索引的数据
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>数据对象,索引无效时返回 default</returns>
public void RegisterItemRender<TItemRender>(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];
}
/// <summary>
/// 添加单个数据项
/// </summary>
/// <param name="item">要添加的数据项</param>
public void Add(T item)
{
list.Add(item);
NotifyDataChanged();
}
/// <summary>
/// 批量添加数据项
/// </summary>
/// <param name="collection">要添加的数据集合</param>
public void AddRange(IEnumerable<T> collection)
{
list.AddRange(collection);
NotifyDataChanged();
}
/// <summary>
/// 在指定位置插入数据项
/// </summary>
/// <param name="index">插入位置</param>
/// <param name="item">要插入的数据项</param>
public void Insert(int index, T item)
{
list.Insert(index, item);
NotifyDataChanged();
}
/// <summary>
/// 在指定位置批量插入数据项
/// </summary>
/// <param name="index">插入位置</param>
/// <param name="collection">要插入的数据集合</param>
public void InsertRange(int index, IEnumerable<T> collection)
{
list.InsertRange(index, collection);
NotifyDataChanged();
}
/// <summary>
/// 移除指定的数据项
/// </summary>
/// <param name="item">要移除的数据项</param>
public void Remove(T item)
{
int index = list.IndexOf(item);
RemoveAt(index);
}
/// <summary>
/// 移除指定索引位置的数据项
/// </summary>
/// <param name="index">要移除的索引</param>
public void RemoveAt(int index)
{
if (index < 0 || index >= GetItemCount()) return;
@ -193,112 +202,218 @@ namespace AlicizaX.UI
NotifyDataChanged();
}
/// <summary>
/// 移除指定范围的数据项
/// </summary>
/// <param name="index">起始索引</param>
/// <param name="count">移除数量</param>
public void RemoveRange(int index, int count)
{
list.RemoveRange(index, count);
NotifyDataChanged();
}
/// <summary>
/// 移除所有符合条件的数据项
/// </summary>
/// <param name="match">匹配条件</param>
public void RemoveAll(Predicate<T> match)
{
list.RemoveAll(match);
NotifyDataChanged();
}
/// <summary>
/// 清空所有数据
/// </summary>
public void Clear()
{
list.Clear();
NotifyDataChanged();
}
/// <summary>
/// 反转指定范围的数据项顺序
/// </summary>
/// <param name="index">起始索引</param>
/// <param name="count">反转数量</param>
public void Reverse(int index, int count)
{
list.Reverse(index, count);
NotifyDataChanged();
}
/// <summary>
/// 反转所有数据项顺序
/// </summary>
public void Reverse()
{
list.Reverse();
NotifyDataChanged();
}
/// <summary>
/// 使用指定的比较器排序数据
/// </summary>
/// <param name="comparison">比较器</param>
public void Sort(Comparison<T> comparison)
{
list.Sort(comparison);
NotifyDataChanged();
}
/// <summary>
/// 设置列表项点击回调
/// </summary>
/// <param name="onItemClick">点击回调</param>
public void SetOnItemClick(Action<T> onItemClick)
{
this.onItemClick = onItemClick;
}
/// <summary>
/// 设置选中项索引
/// </summary>
/// <param name="index">要选中的索引</param>
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);
}
}
/// <summary>
/// 尝试获取指定索引的视图持有者
/// </summary>
/// <param name="index">数据索引</param>
/// <param name="viewHolder">输出的视图持有者</param>
/// <returns>是否成功获取</returns>
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) ? "<default>" : 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<ViewHolder> 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<ViewHolder>();
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);
}
}
}

View File

@ -6,7 +6,7 @@ namespace AlicizaX.UI
public class GroupAdapter<TData> : Adapter<TData> where TData : IGroupViewData, new()
{
private readonly List<TData> 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<TData> list, Action<TData> 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);
};
}
}
}

View File

@ -31,6 +31,8 @@ namespace AlicizaX.UI
/// <param name="index">数据索引</param>
void OnBindViewHolder(ViewHolder viewHolder, int index);
void OnRecycleViewHolder(ViewHolder viewHolder);
/// <summary>
/// 通知数据已更改,触发视图刷新
/// </summary>

View File

@ -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<TData, THolder> : 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<Type, ItemRenderDefinition> 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<TData, THolder>.");
}
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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5baf3050ddf617e4aa583b8ec23f82ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<T> list, Action<T> onItemClick) : base(recyclerView, list, onItemClick)
{
}
public override int GetItemCount()
{
return int.MaxValue;

View File

@ -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<TData> list, Action<TData> onItemClick) : base(recyclerView, list, onItemClick)
{
}
public override string GetViewName(int index)
{
return list[index].TemplateName;

View File

@ -3,30 +3,14 @@ using System.Collections.Generic;
namespace AlicizaX.UI
{
/// <summary>
/// UGList 基类
/// 提供简化的列表操作接口,封装 RecyclerView 和 Adapter 的交互
/// </summary>
/// <typeparam name="TData">数据类型</typeparam>
/// <typeparam name="TAdapter">适配器类型</typeparam>
public abstract class UGListBase<TData, TAdapter> where TAdapter : Adapter<TData> where TData : ISimpleViewData
{
protected readonly RecyclerView _recyclerView;
protected readonly TAdapter _adapter;
/// <summary>
/// 获取关联的 RecyclerView 实例
/// </summary>
public RecyclerView RecyclerView => _recyclerView;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">RecyclerView 实例</param>
/// <param name="adapter">适配器实例</param>
/// <param name="onItemClick">列表项点击回调</param>
public UGListBase(RecyclerView recyclerView, TAdapter adapter, Action<TData> 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);
}
}
/// <summary>
/// 获取适配器实例
/// </summary>
public TAdapter Adapter => _adapter;
public void RegisterItemRender<TItemRender>(string viewName = "") where TItemRender : IItemRender
{
_adapter.RegisterItemRender<TItemRender>(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<TData> _datas;
/// <summary>
/// 获取或设置数据列表
/// 设置时会自动更新适配器
/// </summary>
public List<TData> Data
{
get => _datas;
@ -64,108 +56,50 @@ namespace AlicizaX.UI
}
}
/// <summary>
/// 通用列表类
/// 用于显示简单的单一类型数据列表
/// </summary>
/// <typeparam name="TData">数据类型,必须实现 ISimpleViewData</typeparam>
public class UGList<TData> : UGListBase<TData, Adapter<TData>> where TData : ISimpleViewData
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">RecyclerView 实例</param>
/// <param name="onItemClick">列表项点击回调</param>
public UGList(RecyclerView recyclerView, Action<TData> onItemClick = null)
: base(recyclerView, new Adapter<TData>(recyclerView), onItemClick)
public UGList(RecyclerView recyclerView)
: base(recyclerView, new Adapter<TData>(recyclerView))
{
}
}
/// <summary>
/// 分组列表类
/// 用于显示带有分组头的列表数据
/// </summary>
/// <typeparam name="TData">数据类型,必须实现 IGroupViewData</typeparam>
public class UGGroupList<TData> : UGListBase<TData, GroupAdapter<TData>> where TData : class, IGroupViewData, new()
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">RecyclerView 实例</param>
/// <param name="groupViewName">分组头视图名称</param>
/// <param name="onItemClick">列表项点击回调</param>
public UGGroupList(RecyclerView recyclerView, string groupViewName, Action<TData> onItemClick = null)
: base(recyclerView, new GroupAdapter<TData>(recyclerView, groupViewName), onItemClick)
public UGGroupList(RecyclerView recyclerView, string groupViewName)
: base(recyclerView, new GroupAdapter<TData>(recyclerView, groupViewName))
{
}
}
/// <summary>
/// 循环列表类
/// 用于实现无限循环滚动的列表
/// </summary>
/// <typeparam name="TData">数据类型,必须实现 ISimpleViewData</typeparam>
public class UGLoopList<TData> : UGListBase<TData, LoopAdapter<TData>> where TData : ISimpleViewData, new()
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">RecyclerView 实例</param>
/// <param name="onItemClick">列表项点击回调</param>
public UGLoopList(RecyclerView recyclerView, Action<TData> onItemClick = null)
: base(recyclerView, new LoopAdapter<TData>(recyclerView), onItemClick)
public UGLoopList(RecyclerView recyclerView)
: base(recyclerView, new LoopAdapter<TData>(recyclerView))
{
}
}
/// <summary>
/// 混合列表类
/// 用于显示多种不同类型的列表项
/// </summary>
/// <typeparam name="TData">数据类型,必须实现 IMixedViewData</typeparam>
public class UGMixedList<TData> : UGListBase<TData, MixedAdapter<TData>> where TData : IMixedViewData
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">RecyclerView 实例</param>
/// <param name="onItemClick">列表项点击回调</param>
public UGMixedList(RecyclerView recyclerView, Action<TData> onItemClick = null)
: base(recyclerView, new MixedAdapter<TData>(recyclerView), onItemClick)
public UGMixedList(RecyclerView recyclerView)
: base(recyclerView, new MixedAdapter<TData>(recyclerView))
{
}
}
/// <summary>
/// UGList 创建辅助类
/// 提供便捷的静态方法来创建各种类型的列表
/// </summary>
public static class UGListCreateHelper
{
/// <summary>
/// 创建通用列表
/// </summary>
public static UGList<TData> Create<TData>(RecyclerView recyclerView, Action<TData> onItemClick = null) where TData : ISimpleViewData
=> new UGList<TData>(recyclerView, onItemClick);
public static UGList<TData> Create<TData>(RecyclerView recyclerView) where TData : ISimpleViewData
=> new UGList<TData>(recyclerView);
/// <summary>
/// 创建分组列表
/// </summary>
public static UGGroupList<TData> CreateGroup<TData>(RecyclerView recyclerView, string groupViewName, Action<TData> onItemClick = null) where TData : class, IGroupViewData, new()
=> new UGGroupList<TData>(recyclerView, groupViewName, onItemClick);
public static UGGroupList<TData> CreateGroup<TData>(RecyclerView recyclerView, string groupViewName) where TData : class, IGroupViewData, new()
=> new UGGroupList<TData>(recyclerView, groupViewName);
/// <summary>
/// 创建循环列表
/// </summary>
public static UGLoopList<TData> CreateLoop<TData>(RecyclerView recyclerView, Action<TData> onItemClick = null) where TData : ISimpleViewData, new()
=> new UGLoopList<TData>(recyclerView, onItemClick);
public static UGLoopList<TData> CreateLoop<TData>(RecyclerView recyclerView) where TData : ISimpleViewData, new()
=> new UGLoopList<TData>(recyclerView);
/// <summary>
/// 创建混合列表
/// </summary>
public static UGMixedList<TData> CreateMixed<TData>(RecyclerView recyclerView, Action<TData> onItemClick = null) where TData : IMixedViewData
=> new UGMixedList<TData>(recyclerView, onItemClick);
public static UGMixedList<TData> CreateMixed<TData>(RecyclerView recyclerView) where TData : IMixedViewData
=> new UGMixedList<TData>(recyclerView);
}
}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 491a09d781095104989abb7a91424008
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,20 +1,17 @@
using System;
using AlicizaX.UI.Extension;
using UnityEngine;
using UnityEngine.UI;
namespace AlicizaX.UI
{
/// <summary>
/// 视图持有者基类,用于缓存和复用列表项视图
/// </summary>
public abstract class ViewHolder : MonoBehaviour
{
private RectTransform rectTransform;
private Action clickAction;
private Action pointerEnterAction;
private Action pointerExitAction;
internal event Action<ViewHolder> Destroyed;
/// <summary>
/// 获取 RectTransform 组件
/// </summary>
public RectTransform RectTransform
{
get
@ -26,83 +23,59 @@ namespace AlicizaX.UI
return rectTransform;
}
private set { rectTransform = value; }
private set => rectTransform = value;
}
/// <summary>
/// 视图名称,用于区分不同类型的视图
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// 当前绑定的数据索引
/// </summary>
public int Index { get; internal set; }
/// <summary>
/// 选中状态
/// </summary>
public bool ChoiseState { private set; get; }
/// <summary>
/// 获取视图的尺寸
/// </summary>
public Vector2 SizeDelta => RectTransform.sizeDelta;
/// <summary>
/// 视图首次创建时调用
/// </summary>
protected internal virtual void OnStart()
{
}
/// <summary>
/// 视图被回收到对象池时调用
/// </summary>
protected internal virtual void OnRecycled()
{
}
/// <summary>
/// 绑定视图数据(抽象方法,子类必须实现)
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="data">要绑定的数据</param>
public abstract void BindViewData<T>(T data);
/// <summary>
/// 绑定列表项点击事件
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="data">数据对象</param>
/// <param name="action">点击回调</param>
protected internal virtual void BindItemClick<T>(T data, Action<T> 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;
}
/// <summary>
/// 绑定选中状态
/// </summary>
/// <param name="state">是否选中</param>
protected internal void BindChoiceState(bool state)
public void ClearInteractionCallbacks()
{
if (ChoiseState != state)
{
ChoiseState = state;
OnBindChoiceState(state);
}
clickAction = null;
pointerEnterAction = null;
pointerExitAction = null;
}
/// <summary>
/// 选中状态改变时的回调(可在子类中重写)
/// </summary>
/// <param name="state">是否选中</param>
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;
}
}
}

View File

@ -57,6 +57,7 @@ namespace AlicizaX.UI
public override void Reset()
{
Clear();
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
objectPool.Dispose();
}
}

View File

@ -45,6 +45,7 @@ namespace AlicizaX.UI
public override void Reset()
{
Clear();
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
objectPool.Dispose();
}
}

View File

@ -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);
}