RecyclerView 大优化

优化RecycleView 渲染架构
优化RecyclerView 渲染性能 增加 缓存 异步
增加Navagation导航锚点相关
This commit is contained in:
陈思海 2026-03-31 15:18:50 +08:00
parent 0ed273ba9e
commit dc8c840d69
51 changed files with 3274 additions and 891 deletions

View File

@ -44,7 +44,7 @@ namespace AlicizaX.UI
public Adapter(RecyclerView recyclerView, List<T> list) public Adapter(RecyclerView recyclerView, List<T> list)
{ {
this.recyclerView = recyclerView; this.recyclerView = recyclerView;
this.list = list; this.list = list ?? new List<T>();
} }
public virtual int GetItemCount() public virtual int GetItemCount()
@ -68,10 +68,19 @@ namespace AlicizaX.UI
if (!TryGetBindData(index, out var data)) return; if (!TryGetBindData(index, out var data)) return;
string viewName = GetViewName(index); string viewName = GetViewName(index);
Action defaultClickAction = CreateItemClickAction(index, data); viewHolder.AdvanceBindingVersion();
viewHolder.DataIndex = index;
if (TryGetOrCreateItemRender(viewHolder, viewName, out var itemRender)) if (TryGetOrCreateItemRender(viewHolder, viewName, out var itemRender))
{ {
itemRender.Bind(data, index, defaultClickAction); if (itemRender is ITypedItemRender<T> typedItemRender)
{
typedItemRender.BindData(data, index);
}
else
{
itemRender.Bind(data, index);
}
itemRender.UpdateSelection(index == choiceIndex); itemRender.UpdateSelection(index == choiceIndex);
return; return;
} }
@ -90,24 +99,97 @@ namespace AlicizaX.UI
itemRender.UpdateSelection(false); itemRender.UpdateSelection(false);
itemRender.Unbind(); itemRender.Unbind();
} }
viewHolder.ClearInteractionCallbacks();
} }
public virtual void NotifyDataChanged() public virtual void NotifyDataChanged()
{ {
CoerceChoiceIndex();
recyclerView.RequestLayout(); recyclerView.RequestLayout();
recyclerView.Refresh(); recyclerView.Refresh();
} }
public virtual void SetList(List<T> list) public virtual void SetList(List<T> list)
{ {
this.list = list; this.list = list ?? new List<T>();
recyclerView.Reset(); recyclerView.Reset();
NotifyDataChanged(); NotifyDataChanged();
} }
public void RegisterItemRender<TItemRender>(string viewName = "") where TItemRender : IItemRender public virtual void NotifyItemChanged(int index, bool relayout = false)
{
if (index < 0 || index >= GetRealCount())
{
return;
}
CoerceChoiceIndex();
if (relayout)
{
recyclerView.RequestLayout();
recyclerView.Refresh();
return;
}
recyclerView.RebindVisibleDataIndex(index);
}
public virtual void NotifyItemRangeChanged(int index, int count, bool relayout = false)
{
if (count <= 0 || index < 0 || index >= GetRealCount())
{
return;
}
CoerceChoiceIndex();
if (relayout)
{
recyclerView.RequestLayout();
recyclerView.Refresh();
return;
}
recyclerView.RebindVisibleDataRange(index, count);
}
public virtual void NotifyItemInserted(int index)
{
CoerceChoiceIndex();
recyclerView.RequestLayout();
recyclerView.Refresh();
}
public virtual void NotifyItemRangeInserted(int index, int count)
{
if (count <= 0)
{
return;
}
CoerceChoiceIndex();
recyclerView.RequestLayout();
recyclerView.Refresh();
}
public virtual void NotifyItemRemoved(int index)
{
CoerceChoiceIndex();
recyclerView.RequestLayout();
recyclerView.Refresh();
}
public virtual void NotifyItemRangeRemoved(int index, int count)
{
if (count <= 0)
{
return;
}
CoerceChoiceIndex();
recyclerView.RequestLayout();
recyclerView.Refresh();
}
public void RegisterItemRender<TItemRender>(string viewName = "") where TItemRender : ItemRenderBase
{ {
RegisterItemRender(typeof(TItemRender), viewName); RegisterItemRender(typeof(TItemRender), viewName);
} }
@ -166,25 +248,53 @@ namespace AlicizaX.UI
public void Add(T item) public void Add(T item)
{ {
if (list == null)
{
list = new List<T>();
}
list.Add(item); list.Add(item);
NotifyDataChanged(); NotifyItemInserted(list.Count - 1);
} }
public void AddRange(IEnumerable<T> collection) public void AddRange(IEnumerable<T> collection)
{ {
if (collection == null)
{
return;
}
int startIndex = list.Count;
list.AddRange(collection); list.AddRange(collection);
if (collection is ICollection<T> itemCollection)
{
NotifyItemRangeInserted(startIndex, itemCollection.Count);
return;
}
NotifyDataChanged(); NotifyDataChanged();
} }
public void Insert(int index, T item) public void Insert(int index, T item)
{ {
list.Insert(index, item); list.Insert(index, item);
NotifyDataChanged(); NotifyItemInserted(index);
} }
public void InsertRange(int index, IEnumerable<T> collection) public void InsertRange(int index, IEnumerable<T> collection)
{ {
if (collection == null)
{
return;
}
list.InsertRange(index, collection); list.InsertRange(index, collection);
if (collection is ICollection<T> itemCollection)
{
NotifyItemRangeInserted(index, itemCollection.Count);
return;
}
NotifyDataChanged(); NotifyDataChanged();
} }
@ -199,13 +309,13 @@ namespace AlicizaX.UI
if (index < 0 || index >= GetItemCount()) return; if (index < 0 || index >= GetItemCount()) return;
list.RemoveAt(index); list.RemoveAt(index);
NotifyDataChanged(); NotifyItemRemoved(index);
} }
public void RemoveRange(int index, int count) public void RemoveRange(int index, int count)
{ {
list.RemoveRange(index, count); list.RemoveRange(index, count);
NotifyDataChanged(); NotifyItemRangeRemoved(index, count);
} }
public void RemoveAll(Predicate<T> match) public void RemoveAll(Predicate<T> match)
@ -216,8 +326,14 @@ namespace AlicizaX.UI
public void Clear() public void Clear()
{ {
if (list == null || list.Count == 0)
{
return;
}
int count = list.Count;
list.Clear(); list.Clear();
NotifyDataChanged(); NotifyItemRangeRemoved(0, count);
} }
public void Reverse(int index, int count) public void Reverse(int index, int count)
@ -240,6 +356,20 @@ namespace AlicizaX.UI
protected void SetChoiceIndex(int index) protected void SetChoiceIndex(int index)
{ {
int itemCount = GetRealCount();
if (itemCount <= 0)
{
index = -1;
}
else if (index >= itemCount)
{
index = itemCount - 1;
}
else if (index < -1)
{
index = -1;
}
if (index == choiceIndex) return; if (index == choiceIndex) return;
if (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var oldHolder)) if (choiceIndex != -1 && TryGetViewHolder(choiceIndex, out var oldHolder))
@ -255,12 +385,6 @@ namespace AlicizaX.UI
} }
} }
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) protected virtual bool TryGetBindData(int index, out T data)
{ {
if (list == null || index < 0 || index >= list.Count) if (list == null || index < 0 || index >= list.Count)
@ -273,9 +397,10 @@ namespace AlicizaX.UI
return true; return true;
} }
protected virtual Action CreateItemClickAction(int index, T data) private bool TryGetViewHolder(int index, out ViewHolder viewHolder)
{ {
return () => { SetChoiceIndex(index); }; viewHolder = recyclerView.ViewProvider.GetViewHolderByDataIndex(index);
return viewHolder != null;
} }
private bool TryGetItemRenderDefinition(string viewName, out ItemRenderResolver.ItemRenderDefinition definition) private bool TryGetItemRenderDefinition(string viewName, out ItemRenderResolver.ItemRenderDefinition definition)
@ -321,6 +446,20 @@ namespace AlicizaX.UI
return false; return false;
} }
private static void ReleaseItemRender(ItemRenderEntry entry)
{
if (entry?.ItemRender == null)
{
return;
}
entry.ItemRender.Unbind();
if (entry.ItemRender is ItemRenderBase itemRender)
{
itemRender.Detach();
}
}
private bool TryGetOrCreateItemRender(ViewHolder viewHolder, string viewName, out IItemRender itemRender) private bool TryGetOrCreateItemRender(ViewHolder viewHolder, string viewName, out IItemRender itemRender)
{ {
if (viewHolder == null) if (viewHolder == null)
@ -337,7 +476,7 @@ namespace AlicizaX.UI
return true; return true;
} }
entry.ItemRender?.Unbind(); ReleaseItemRender(entry);
viewHolder.Destroyed -= OnViewHolderDestroyed; viewHolder.Destroyed -= OnViewHolderDestroyed;
itemRenders.Remove(viewHolder); itemRenders.Remove(viewHolder);
} }
@ -348,7 +487,7 @@ namespace AlicizaX.UI
return false; return false;
} }
itemRender = definition.Create(viewHolder); itemRender = definition.Create(viewHolder, recyclerView, this, SetChoiceIndex);
itemRenders[viewHolder] = new ItemRenderEntry(viewName, itemRender); itemRenders[viewHolder] = new ItemRenderEntry(viewName, itemRender);
viewHolder.Destroyed += OnViewHolderDestroyed; viewHolder.Destroyed += OnViewHolderDestroyed;
return true; return true;
@ -363,7 +502,7 @@ namespace AlicizaX.UI
{ {
foreach (var pair in itemRenders) foreach (var pair in itemRenders)
{ {
pair.Value.ItemRender?.Unbind(); ReleaseItemRender(pair.Value);
if (pair.Key != null) if (pair.Key != null)
{ {
pair.Key.Destroyed -= OnViewHolderDestroyed; pair.Key.Destroyed -= OnViewHolderDestroyed;
@ -388,7 +527,7 @@ namespace AlicizaX.UI
continue; continue;
} }
pair.Value.ItemRender?.Unbind(); ReleaseItemRender(pair.Value);
pair.Key.Destroyed -= OnViewHolderDestroyed; pair.Key.Destroyed -= OnViewHolderDestroyed;
viewHoldersToRemove ??= new List<ViewHolder>(); viewHoldersToRemove ??= new List<ViewHolder>();
viewHoldersToRemove.Add(pair.Key); viewHoldersToRemove.Add(pair.Key);
@ -413,7 +552,26 @@ namespace AlicizaX.UI
} }
viewHolder.Destroyed -= OnViewHolderDestroyed; viewHolder.Destroyed -= OnViewHolderDestroyed;
if (itemRenders.TryGetValue(viewHolder, out var entry))
{
ReleaseItemRender(entry);
itemRenders.Remove(viewHolder); itemRenders.Remove(viewHolder);
} }
} }
private void CoerceChoiceIndex()
{
int itemCount = GetRealCount();
if (itemCount <= 0)
{
SetChoiceIndex(-1);
return;
}
if (choiceIndex >= itemCount)
{
SetChoiceIndex(itemCount - 1);
}
}
}
} }

View File

@ -28,35 +28,47 @@ namespace AlicizaX.UI
public override string GetViewName(int index) public override string GetViewName(int index)
{ {
return showList[index].TemplateName; return index >= 0 && index < showList.Count
? showList[index].TemplateName
: string.Empty;
} }
public override void NotifyDataChanged() public override void NotifyDataChanged()
{ {
foreach (var data in list) if (string.IsNullOrEmpty(groupViewName))
{ {
CreateGroup(data.Type); throw new InvalidOperationException("GroupAdapter requires a non-empty groupViewName.");
} }
var groupList = showList.FindAll(data => data.TemplateName == groupViewName); if (list == null)
for (int i = 0; i < groupList.Count; i++)
{ {
int index = showList.IndexOf(groupList[i]); showList.Clear();
Collapse(index); base.NotifyDataChanged();
if (groupList[i].Expanded) return;
}
for (int i = 0; i < list.Count; i++)
{ {
Expand(index); CreateGroup(list[i].Type);
} }
}
for (int i = 0; i < showList.Count; i++)
foreach (var group in groupList) {
{ TData group = showList[i];
if (list.FindAll(data => data.Type == group.Type).Count == 0) if (group.TemplateName != groupViewName)
{ {
showList.Remove(group); continue;
}
Collapse(i);
if (group.Expanded)
{
Expand(i);
i += CountItemsForType(group.Type);
} }
} }
RemoveEmptyGroups();
base.NotifyDataChanged(); base.NotifyDataChanged();
} }
@ -82,14 +94,45 @@ namespace AlicizaX.UI
public void Expand(int index) public void Expand(int index)
{ {
var expandList = list.FindAll(data => data.Type == showList[index].Type); if (list == null || index < 0 || index >= showList.Count)
showList.InsertRange(index + 1, expandList); {
return;
}
int type = showList[index].Type;
for (int i = 0; i < list.Count; i++)
{
if (list[i].Type == type)
{
showList.Insert(index + 1, list[i]);
index++;
}
}
} }
public void Collapse(int index) public void Collapse(int index)
{ {
var collapseList = showList.FindAll(data => data.Type == showList[index].Type && data.TemplateName != groupViewName); if (index < 0 || index >= showList.Count)
showList.RemoveRange(index + 1, collapseList.Count); {
return;
}
int type = showList[index].Type;
int removeCount = 0;
for (int i = index + 1; i < showList.Count; i++)
{
if (showList[i].TemplateName == groupViewName || showList[i].Type != type)
{
break;
}
removeCount++;
}
if (removeCount > 0)
{
showList.RemoveRange(index + 1, removeCount);
}
} }
protected override bool TryGetBindData(int index, out TData data) protected override bool TryGetBindData(int index, out TData data)
@ -104,10 +147,14 @@ namespace AlicizaX.UI
return true; return true;
} }
protected override Action CreateItemClickAction(int index, TData data) public void Activate(int index)
{ {
return () => if (index < 0 || index >= showList.Count)
{ {
return;
}
TData data = showList[index];
if (data.TemplateName == groupViewName) if (data.TemplateName == groupViewName)
{ {
data.Expanded = !data.Expanded; data.Expanded = !data.Expanded;
@ -116,7 +163,42 @@ namespace AlicizaX.UI
} }
SetChoiceIndex(index); SetChoiceIndex(index);
}; }
private int CountItemsForType(int type)
{
if (list == null)
{
return 0;
}
int count = 0;
for (int i = 0; i < list.Count; i++)
{
if (list[i].Type == type)
{
count++;
}
}
return count;
}
private void RemoveEmptyGroups()
{
for (int i = showList.Count - 1; i >= 0; i--)
{
TData group = showList[i];
if (group.TemplateName != groupViewName)
{
continue;
}
if (CountItemsForType(group.Type) == 0)
{
showList.RemoveAt(i);
}
}
} }
} }
} }

View File

@ -1,41 +1,17 @@
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
/// <summary>
/// RecyclerView 适配器接口,负责提供数据和绑定视图
/// </summary>
public interface IAdapter public interface IAdapter
{ {
/// <summary>
/// 获取列表项总数(包括循环或分组后的虚拟数量)
/// </summary>
/// <returns>列表项总数</returns>
int GetItemCount(); int GetItemCount();
/// <summary>
/// 获取实际数据项数量(不包括循环或分组的虚拟数量)
/// </summary>
/// <returns>实际数据项数量</returns>
int GetRealCount(); int GetRealCount();
/// <summary>
/// 获取指定索引位置的视图名称,用于视图类型区分
/// </summary>
/// <param name="index">列表项索引</param>
/// <returns>视图名称</returns>
string GetViewName(int index); string GetViewName(int index);
/// <summary>
/// 绑定视图持有者与数据
/// </summary>
/// <param name="viewHolder">视图持有者</param>
/// <param name="index">数据索引</param>
void OnBindViewHolder(ViewHolder viewHolder, int index); void OnBindViewHolder(ViewHolder viewHolder, int index);
void OnRecycleViewHolder(ViewHolder viewHolder); void OnRecycleViewHolder(ViewHolder viewHolder);
/// <summary>
/// 通知数据已更改,触发视图刷新
/// </summary>
void NotifyDataChanged(); void NotifyDataChanged();
} }
} }

View File

@ -1,108 +1,364 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using UnityEngine.EventSystems;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
public interface IItemRender /// <summary>
/// 定义 ItemRender 的基础绑定与解绑协议。
/// </summary>
internal interface IItemRender
{ {
void Bind(object data, int index, Action defaultClickAction); /// <summary>
/// 将指定数据绑定到当前渲染实例。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
void Bind(object data, int index);
/// <summary>
/// 更新当前渲染实例的选中状态。
/// </summary>
/// <param name="selected">是否处于选中状态。</param>
void UpdateSelection(bool selected); void UpdateSelection(bool selected);
/// <summary>
/// 清理当前渲染实例上的绑定状态。
/// </summary>
void Unbind(); void Unbind();
} }
internal interface IItemRenderInitializer /// <summary>
/// 定义带强类型数据绑定能力的 ItemRender 协议。
/// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
internal interface ITypedItemRender<in TData> : IItemRender
{ {
void Reset(ViewHolder viewHolder); /// <summary>
/// 使用强类型数据执行绑定。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
void BindData(TData data, int index);
} }
public abstract class ItemRender<TData, THolder> : IItemRender /// <summary>
, IItemRenderInitializer /// 提供 ItemRender 的公共基类,封装框架内部的绑定生命周期入口。
/// </summary>
public abstract class ItemRenderBase : IItemRender
{
/// <summary>
/// 将渲染实例附加到指定的视图持有者。
/// </summary>
/// <param name="viewHolder">目标视图持有者。</param>
/// <param name="recyclerView">所属的 RecyclerView。</param>
/// <param name="adapter">当前使用的适配器。</param>
/// <param name="selectionHandler">选中项变更回调。</param>
internal abstract void Attach(ViewHolder viewHolder, RecyclerView recyclerView, IAdapter adapter, Action<int> selectionHandler);
/// <summary>
/// 将渲染实例从当前视图持有者上分离。
/// </summary>
internal abstract void Detach();
/// <summary>
/// 以对象形式绑定数据。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
internal abstract void BindObject(object data, int index);
/// <summary>
/// 更新内部记录的选中状态。
/// </summary>
/// <param name="selected">是否处于选中状态。</param>
internal abstract void UpdateSelectionInternal(bool selected);
/// <summary>
/// 清理当前绑定产生的临时状态。
/// </summary>
internal abstract void UnbindInternal();
/// <summary>
/// 由框架内部调用,将对象数据绑定到当前渲染实例。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
void IItemRender.Bind(object data, int index)
{
BindObject(data, index);
}
/// <summary>
/// 由框架内部调用,更新当前渲染实例的选中状态。
/// </summary>
/// <param name="selected">是否处于选中状态。</param>
void IItemRender.UpdateSelection(bool selected)
{
UpdateSelectionInternal(selected);
}
/// <summary>
/// 由框架内部调用,清理当前渲染实例的绑定状态。
/// </summary>
void IItemRender.Unbind()
{
UnbindInternal();
}
}
/// <summary>
/// 提供带强类型数据与视图持有者的列表项渲染基类。
/// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
/// <typeparam name="THolder">视图持有者类型。</typeparam>
public abstract class ItemRender<TData, THolder> : ItemRenderBase, IItemInteractionHost, ITypedItemRender<TData>
where THolder : ViewHolder where THolder : ViewHolder
{ {
private Action defaultClickAction; /// <summary>
/// 当前持有者上的交互代理组件。
/// </summary>
private ItemInteractionProxy interactionProxy;
/// <summary>
/// 当前项被选中时的回调委托。
/// </summary>
private Action<int> selectionHandler;
/// <summary>
/// 上一次绑定到交互代理的交互标记。
/// </summary>
private ItemInteractionFlags cachedInteractionFlags;
/// <summary>
/// 标记交互代理是否已经完成当前配置绑定。
/// </summary>
private bool interactionBindingActive;
/// <summary>
/// 获取当前附加的强类型视图持有者。
/// </summary>
protected THolder Holder { get; private set; } protected THolder Holder { get; private set; }
/// <summary>
/// 获取当前所属的 RecyclerView。
/// </summary>
protected RecyclerView RecyclerView { get; private set; }
/// <summary>
/// 获取当前所属的适配器。
/// </summary>
protected IAdapter Adapter { get; private set; }
/// <summary>
/// 获取当前绑定的数据对象。
/// </summary>
protected TData CurrentData { get; private set; } protected TData CurrentData { get; private set; }
/// <summary>
/// 获取当前绑定的数据索引。
/// </summary>
protected int CurrentIndex { get; private set; } = -1; protected int CurrentIndex { get; private set; } = -1;
/// <summary>
/// 获取当前绑定的布局索引。
/// </summary>
protected int CurrentLayoutIndex { get; private set; } = -1;
/// <summary>
/// 获取当前项是否处于选中状态。
/// </summary>
protected bool IsSelected { get; private set; } protected bool IsSelected { get; private set; }
public void Bind(object data, int index, Action defaultClickAction) /// <summary>
{ /// 获取当前绑定版本号,用于校验异步回调是否仍然有效。
EnsureHolder(); /// </summary>
protected uint CurrentBindingVersion { get; private set; }
/// <summary>
/// 获取当前渲染项支持的交互能力。
/// </summary>
public virtual ItemInteractionFlags InteractionFlags => ItemInteractionFlags.None;
/// <summary>
/// 由框架交互代理读取当前渲染项的交互能力。
/// </summary>
ItemInteractionFlags IItemInteractionHost.InteractionFlags => InteractionFlags;
/// <summary>
/// 获取键盘或手柄导航时采用的移动选项。
/// </summary>
protected virtual RecyclerNavigationOptions NavigationOptions => RecyclerNavigationOptions.Circular;
/// <summary>
/// 以对象形式绑定数据并执行强类型校验。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
internal override void BindObject(object data, int index)
{
if (data is not TData itemData) if (data is not TData itemData)
{ {
throw new InvalidCastException( throw new InvalidCastException(
$"ItemRender '{GetType().Name}' expected data '{typeof(TData).Name}', but got '{data?.GetType().Name ?? "null"}'."); $"ItemRender '{GetType().Name}' expected data '{typeof(TData).Name}', but got '{data?.GetType().Name ?? "null"}'.");
} }
CurrentData = itemData; BindCore(itemData, index);
CurrentIndex = index;
this.defaultClickAction = defaultClickAction;
Holder.SetInteractionCallbacks(HandleClick, HandlePointerEnter, HandlePointerExit);
Bind(itemData, index);
} }
public void UpdateSelection(bool selected) /// <summary>
/// 由框架内部调用,使用强类型数据执行绑定。
/// </summary>
/// <param name="data">待绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
void ITypedItemRender<TData>.BindData(TData data, int index)
{
BindCore(data, index);
}
/// <summary>
/// 更新内部选中状态并触发选中状态回调。
/// </summary>
/// <param name="selected">是否处于选中状态。</param>
internal override void UpdateSelectionInternal(bool selected)
{ {
EnsureHolder(); EnsureHolder();
IsSelected = selected; IsSelected = selected;
OnSelectionChanged(selected); OnSelectionChanged(selected);
} }
public void Unbind() /// <summary>
/// 清理当前绑定数据关联的状态,并重置内部缓存。
/// </summary>
internal override void UnbindInternal()
{ {
ResetState(); if (Holder != null)
{
if (IsSelected)
{
IsSelected = false;
OnSelectionChanged(false);
} }
protected abstract void Bind(TData data, int index); OnClear();
if (interactionProxy != null)
{
interactionProxy.Clear();
interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None;
}
protected virtual void OnHolderChanged() Holder.DataIndex = -1;
}
CurrentData = default;
CurrentIndex = -1;
CurrentLayoutIndex = -1;
CurrentBindingVersion = 0;
IsSelected = false;
}
/// <summary>
/// 判断指定绑定版本是否仍与当前持有者保持一致。
/// </summary>
/// <param name="bindingVersion">待校验的绑定版本号。</param>
/// <returns>版本号仍然有效时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
protected bool IsBindingCurrent(uint bindingVersion)
{
return Holder != null &&
CurrentBindingVersion != 0 &&
CurrentBindingVersion == bindingVersion &&
Holder.BindingVersion == bindingVersion;
}
/// <summary>
/// 执行一次完整的数据绑定流程。
/// </summary>
/// <param name="itemData">待绑定的强类型数据。</param>
/// <param name="index">当前数据索引。</param>
private void BindCore(TData itemData, int index)
{
EnsureHolder();
CurrentData = itemData;
CurrentIndex = index;
CurrentLayoutIndex = Holder.Index;
Holder.DataIndex = index;
CurrentBindingVersion = Holder.BindingVersion;
BindInteractionProxyIfNeeded();
OnBind(itemData, index);
}
/// <summary>
/// 每次当前持有者绑定到新的数据项时调用。
/// 仅用于执行数据驱动的界面刷新,例如文本、图片与状态更新。
/// 不要在此注册持有者级别的事件监听。
/// </summary>
/// <param name="data">当前绑定的数据对象。</param>
/// <param name="index">当前数据索引。</param>
protected abstract void OnBind(TData data, int index);
/// <summary>
/// 当当前渲染实例附加到持有者实例时调用。
/// 这是持有者级生命周期,通常对同一组 render 与 holder 仅触发一次。
/// 适合执行一次性的持有者初始化,例如注册按钮监听或挂接可复用交互组件。
/// </summary>
protected virtual void OnHolderAttached()
{
interactionProxy = Holder.GetComponent<ItemInteractionProxy>();
if (interactionProxy == null)
{
interactionProxy = Holder.gameObject.AddComponent<ItemInteractionProxy>();
}
}
/// <summary>
/// 当当前渲染实例即将从持有者实例分离时调用。
/// 这是持有者级清理生命周期,通常对同一组 render 与 holder 仅触发一次。
/// 适合执行一次性的持有者清理,例如注销按钮监听或释放附加阶段缓存的引用。
/// </summary>
protected virtual void OnHolderDetached()
{ {
} }
/// <summary>
/// 当当前项的选中状态发生变化时调用。
/// 仅应在此更新选中态相关的界面表现。
/// </summary>
/// <param name="selected">当前是否处于选中状态。</param>
protected virtual void OnSelectionChanged(bool selected) protected virtual void OnSelectionChanged(bool selected)
{ {
} }
/// <summary>
/// 每次当前数据绑定被清理时调用。
/// 这是绑定级清理生命周期,在复用过程中可能被多次触发。
/// 适合在此重置由当前绑定数据产生的临时界面状态。
/// </summary>
protected virtual void OnClear() protected virtual void OnClear()
{ {
} }
protected virtual void OnClick() /// <summary>
/// 通知外部选中当前数据项。
/// </summary>
private void SelectCurrentItem()
{ {
if (CurrentIndex >= 0)
{
selectionHandler?.Invoke(CurrentIndex);
}
} }
protected virtual void OnPointerEnter() /// <summary>
{ /// 将当前渲染实例附加到指定持有者,并初始化上下文引用。
} /// </summary>
/// <param name="viewHolder">目标视图持有者。</param>
protected virtual void OnPointerExit() /// <param name="recyclerView">所属的 RecyclerView。</param>
{ /// <param name="adapter">当前使用的适配器。</param>
} /// <param name="selectionHandler">选中项变更回调。</param>
internal override void Attach(ViewHolder viewHolder, RecyclerView recyclerView, IAdapter adapter, Action<int> selectionHandler)
private void HandleClick()
{
defaultClickAction?.Invoke();
OnClick();
}
private void HandlePointerEnter()
{
OnPointerEnter();
}
private void HandlePointerExit()
{
OnPointerExit();
}
void IItemRenderInitializer.Reset(ViewHolder viewHolder)
{ {
if (viewHolder == null) if (viewHolder == null)
{ {
@ -115,11 +371,44 @@ namespace AlicizaX.UI
$"RecyclerView item render '{GetType().FullName}' expects holder '{typeof(THolder).FullName}', but got '{viewHolder.GetType().FullName}'."); $"RecyclerView item render '{GetType().FullName}' expects holder '{typeof(THolder).FullName}', but got '{viewHolder.GetType().FullName}'.");
} }
ResetState();
Holder = holder; Holder = holder;
OnHolderChanged(); RecyclerView = recyclerView;
Adapter = adapter;
this.selectionHandler = selectionHandler;
interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None;
OnHolderAttached();
} }
/// <summary>
/// 将当前渲染实例从持有者上分离,并释放上下文引用。
/// </summary>
internal override void Detach()
{
if (Holder == null)
{
return;
}
OnHolderDetached();
interactionProxy?.Clear();
interactionProxy = null;
selectionHandler = null;
Holder = null;
RecyclerView = null;
Adapter = null;
CurrentData = default;
CurrentIndex = -1;
CurrentLayoutIndex = -1;
CurrentBindingVersion = 0;
IsSelected = false;
interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None;
}
/// <summary>
/// 确保当前渲染实例已经绑定有效的视图持有者。
/// </summary>
private void EnsureHolder() private void EnsureHolder()
{ {
if (Holder == null) if (Holder == null)
@ -129,42 +418,279 @@ namespace AlicizaX.UI
} }
} }
private void ResetState() /// <summary>
/// 按指定方向尝试移动焦点。
/// </summary>
/// <param name="direction">焦点移动方向。</param>
/// <returns>成功移动焦点时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private bool MoveFocus(MoveDirection direction)
{ {
if (Holder != null) return RecyclerView != null && RecyclerView.NavigationController.TryMove(Holder, direction, NavigationOptions);
{
if (IsSelected)
{
IsSelected = false;
OnSelectionChanged(false);
} }
OnClear(); /// <summary>
Holder.ClearInteractionCallbacks(); /// 在需要时将当前渲染实例绑定到交互代理。
/// </summary>
private void BindInteractionProxyIfNeeded()
{
if (interactionProxy == null)
{
return;
} }
CurrentData = default; ItemInteractionFlags interactionFlags = InteractionFlags;
CurrentIndex = -1; if (interactionBindingActive && cachedInteractionFlags == interactionFlags)
IsSelected = false; {
defaultClickAction = null; return;
}
interactionProxy.Bind(this);
cachedInteractionFlags = interactionFlags;
interactionBindingActive = true;
}
/// <summary>
/// 由交互代理转发点击事件。
/// </summary>
/// <param name="eventData">点击事件数据。</param>
void IItemInteractionHost.HandlePointerClick(PointerEventData eventData)
{
SelectCurrentItem();
OnPointerClick(eventData);
}
/// <summary>
/// 由交互代理转发指针进入事件。
/// </summary>
/// <param name="eventData">指针事件数据。</param>
void IItemInteractionHost.HandlePointerEnter(PointerEventData eventData)
{
OnPointerEnter(eventData);
}
/// <summary>
/// 由交互代理转发指针离开事件。
/// </summary>
/// <param name="eventData">指针事件数据。</param>
void IItemInteractionHost.HandlePointerExit(PointerEventData eventData)
{
OnPointerExit(eventData);
}
/// <summary>
/// 由交互代理转发选中事件。
/// </summary>
/// <param name="eventData">选中事件数据。</param>
void IItemInteractionHost.HandleSelect(BaseEventData eventData)
{
OnItemSelected(eventData);
}
/// <summary>
/// 由交互代理转发取消选中事件。
/// </summary>
/// <param name="eventData">取消选中事件数据。</param>
void IItemInteractionHost.HandleDeselect(BaseEventData eventData)
{
OnItemDeselected(eventData);
}
/// <summary>
/// 由交互代理转发导航移动事件。
/// </summary>
/// <param name="eventData">导航事件数据。</param>
void IItemInteractionHost.HandleMove(AxisEventData eventData)
{
if (!OnMove(eventData))
{
MoveFocus(eventData.moveDir);
} }
} }
/// <summary>
/// 由交互代理转发开始拖拽事件。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
void IItemInteractionHost.HandleBeginDrag(PointerEventData eventData)
{
OnBeginDrag(eventData);
}
/// <summary>
/// 由交互代理转发拖拽事件。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
void IItemInteractionHost.HandleDrag(PointerEventData eventData)
{
OnDrag(eventData);
}
/// <summary>
/// 由交互代理转发结束拖拽事件。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
void IItemInteractionHost.HandleEndDrag(PointerEventData eventData)
{
OnEndDrag(eventData);
}
/// <summary>
/// 由交互代理转发提交事件。
/// </summary>
/// <param name="eventData">提交事件数据。</param>
void IItemInteractionHost.HandleSubmit(BaseEventData eventData)
{
SelectCurrentItem();
OnSubmit(eventData);
}
/// <summary>
/// 由交互代理转发取消事件。
/// </summary>
/// <param name="eventData">取消事件数据。</param>
void IItemInteractionHost.HandleCancel(BaseEventData eventData)
{
OnCancel(eventData);
}
/// <summary>
/// 当当前项收到点击事件时调用。
/// </summary>
/// <param name="eventData">点击事件数据。</param>
protected virtual void OnPointerClick(PointerEventData eventData)
{
}
/// <summary>
/// 当指针进入当前项时调用。
/// </summary>
/// <param name="eventData">指针事件数据。</param>
protected virtual void OnPointerEnter(PointerEventData eventData)
{
}
/// <summary>
/// 当指针离开当前项时调用。
/// </summary>
/// <param name="eventData">指针事件数据。</param>
protected virtual void OnPointerExit(PointerEventData eventData)
{
}
/// <summary>
/// 当当前项被 EventSystem 选中时调用。
/// </summary>
/// <param name="eventData">选中事件数据。</param>
protected virtual void OnItemSelected(BaseEventData eventData)
{
}
/// <summary>
/// 当当前项被 EventSystem 取消选中时调用。
/// </summary>
/// <param name="eventData">取消选中事件数据。</param>
protected virtual void OnItemDeselected(BaseEventData eventData)
{
}
/// <summary>
/// 当当前项收到导航移动事件时调用。
/// </summary>
/// <param name="eventData">导航事件数据。</param>
/// <returns>已自行处理导航事件时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
protected virtual bool OnMove(AxisEventData eventData)
{
return false;
}
/// <summary>
/// 当当前项开始被拖拽时调用。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
protected virtual void OnBeginDrag(PointerEventData eventData)
{
}
/// <summary>
/// 当当前项发生拖拽时调用。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
protected virtual void OnDrag(PointerEventData eventData)
{
}
/// <summary>
/// 当当前项结束拖拽时调用。
/// </summary>
/// <param name="eventData">拖拽事件数据。</param>
protected virtual void OnEndDrag(PointerEventData eventData)
{
}
/// <summary>
/// 当当前项收到提交操作时调用。
/// </summary>
/// <param name="eventData">提交事件数据。</param>
protected virtual void OnSubmit(BaseEventData eventData)
{
}
/// <summary>
/// 当当前项收到取消操作时调用。
/// </summary>
/// <param name="eventData">取消事件数据。</param>
protected virtual void OnCancel(BaseEventData eventData)
{
}
}
/// <summary>
/// 负责解析、缓存并创建 ItemRender 定义。
/// </summary>
internal static class ItemRenderResolver internal static class ItemRenderResolver
{ {
/// <summary>
/// 描述单个 ItemRender 的类型信息与创建方式。
/// </summary>
internal sealed class ItemRenderDefinition internal sealed class ItemRenderDefinition
{ {
public ItemRenderDefinition(Type itemRenderType, Type holderType) /// <summary>
/// 初始化一份 ItemRender 定义。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <param name="holderType">对应的持有者类型。</param>
/// <param name="createInstance">渲染器实例创建委托。</param>
public ItemRenderDefinition(Type itemRenderType, Type holderType, Func<IItemRender> createInstance)
{ {
ItemRenderType = itemRenderType; ItemRenderType = itemRenderType;
HolderType = holderType; HolderType = holderType;
this.createInstance = createInstance;
} }
/// <summary>
/// 获取渲染器运行时类型。
/// </summary>
public Type ItemRenderType { get; } public Type ItemRenderType { get; }
/// <summary>
/// 获取渲染器要求的持有者类型。
/// </summary>
public Type HolderType { get; } public Type HolderType { get; }
public IItemRender Create(ViewHolder viewHolder) /// <summary>
/// 用于创建渲染器实例的缓存委托。
/// </summary>
private readonly Func<IItemRender> createInstance;
/// <summary>
/// 创建并初始化一个可用的 ItemRender 实例。
/// </summary>
/// <param name="viewHolder">目标视图持有者。</param>
/// <param name="recyclerView">所属的 RecyclerView。</param>
/// <param name="adapter">当前使用的适配器。</param>
/// <param name="selectionHandler">选中项变更回调。</param>
/// <returns>已初始化完成的渲染器实例。</returns>
public IItemRender Create(ViewHolder viewHolder, RecyclerView recyclerView, IAdapter adapter, Action<int> selectionHandler)
{ {
if (viewHolder == null) if (viewHolder == null)
{ {
@ -177,25 +703,27 @@ namespace AlicizaX.UI
$"RecyclerView item render '{ItemRenderType.FullName}' expects holder '{HolderType.FullName}', but got '{viewHolder.GetType().FullName}'."); $"RecyclerView item render '{ItemRenderType.FullName}' expects holder '{HolderType.FullName}', but got '{viewHolder.GetType().FullName}'.");
} }
if (Activator.CreateInstance(ItemRenderType, true) is not IItemRender itemRender) if (createInstance() is not ItemRenderBase itemRender)
{ {
throw new InvalidOperationException( throw new InvalidOperationException(
$"RecyclerView item render '{ItemRenderType.FullName}' could not be created."); $"RecyclerView item render '{ItemRenderType.FullName}' could not be created.");
} }
if (itemRender is not IItemRenderInitializer initializer) itemRender.Attach(viewHolder, recyclerView, adapter, selectionHandler);
{
throw new InvalidOperationException(
$"RecyclerView item render '{ItemRenderType.FullName}' must inherit from ItemRender<TData, THolder>.");
}
initializer.Reset(viewHolder);
return itemRender; return itemRender;
} }
} }
/// <summary>
/// ItemRender 定义缓存表,键为渲染器类型。
/// </summary>
private static readonly Dictionary<Type, ItemRenderDefinition> Definitions = new(); private static readonly Dictionary<Type, ItemRenderDefinition> Definitions = new();
/// <summary>
/// 获取指定渲染器类型对应的定义,不存在时自动创建并缓存。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <returns>与该类型对应的渲染器定义。</returns>
public static ItemRenderDefinition GetOrCreate(Type itemRenderType) public static ItemRenderDefinition GetOrCreate(Type itemRenderType)
{ {
if (itemRenderType == null) if (itemRenderType == null)
@ -213,6 +741,11 @@ namespace AlicizaX.UI
return definition; return definition;
} }
/// <summary>
/// 为指定渲染器类型构建定义信息。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <returns>创建完成的渲染器定义。</returns>
private static ItemRenderDefinition CreateDefinition(Type itemRenderType) private static ItemRenderDefinition CreateDefinition(Type itemRenderType)
{ {
if (itemRenderType.IsAbstract || if (itemRenderType.IsAbstract ||
@ -242,9 +775,15 @@ namespace AlicizaX.UI
$"RecyclerView item render '{itemRenderType.FullName}' must have a parameterless constructor."); $"RecyclerView item render '{itemRenderType.FullName}' must have a parameterless constructor.");
} }
return new ItemRenderDefinition(itemRenderType, holderType); return new ItemRenderDefinition(itemRenderType, holderType, CreateFactory(constructor));
} }
/// <summary>
/// 尝试从渲染器继承链中解析对应的持有者类型。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <param name="holderType">解析得到的持有者类型。</param>
/// <returns>解析成功时返回 <see langword="true"/>;否则返回 <see langword="false"/>。</returns>
private static bool TryGetHolderType(Type itemRenderType, out Type holderType) private static bool TryGetHolderType(Type itemRenderType, out Type holderType)
{ {
for (Type current = itemRenderType; current != null && current != typeof(object); current = current.BaseType) for (Type current = itemRenderType; current != null && current != typeof(object); current = current.BaseType)
@ -261,5 +800,17 @@ namespace AlicizaX.UI
holderType = null; holderType = null;
return false; return false;
} }
/// <summary>
/// 基于无参构造函数创建渲染器实例工厂。
/// </summary>
/// <param name="constructor">渲染器的无参构造函数。</param>
/// <returns>用于创建渲染器实例的委托。</returns>
private static Func<IItemRender> CreateFactory(ConstructorInfo constructor)
{
NewExpression newExpression = Expression.New(constructor);
UnaryExpression convertExpression = Expression.Convert(newExpression, typeof(IItemRender));
return Expression.Lambda<Func<IItemRender>>(convertExpression).Compile();
}
} }
} }

View File

@ -14,7 +14,7 @@ namespace AlicizaX.UI
public override int GetItemCount() public override int GetItemCount()
{ {
return int.MaxValue; return GetRealCount() > 0 ? int.MaxValue : 0;
} }
public override int GetRealCount() public override int GetRealCount()
@ -24,6 +24,11 @@ namespace AlicizaX.UI
public override void OnBindViewHolder(ViewHolder viewHolder, int index) public override void OnBindViewHolder(ViewHolder viewHolder, int index)
{ {
if (list == null || list.Count == 0)
{
return;
}
index %= list.Count; index %= list.Count;
base.OnBindViewHolder(viewHolder, index); base.OnBindViewHolder(viewHolder, index);
} }

View File

@ -14,7 +14,9 @@ namespace AlicizaX.UI
public override string GetViewName(int index) public override string GetViewName(int index)
{ {
return list[index].TemplateName; return index >= 0 && list != null && index < list.Count
? list[index].TemplateName
: string.Empty;
} }
} }
} }

View File

@ -1,23 +1,12 @@
using System; using System;
/// <summary>
/// 缓动函数工具类
/// 提供各种常用的缓动函数,用于实现平滑的动画效果
/// 基于 https://easings.net/ 的标准缓动函数
/// </summary>
public class EaseUtil public class EaseUtil
{ {
/// <summary>
/// 正弦缓入函数
/// </summary>
public static double EaseInSine(float x) public static double EaseInSine(float x)
{ {
return 1 - Math.Cos(x * Math.PI / 2); return 1 - Math.Cos(x * Math.PI / 2);
} }
/// <summary>
/// 正弦缓出函数
/// </summary>
public static double EaseOutSine(float x) public static double EaseOutSine(float x)
{ {
return Math.Sin(x * Math.PI / 2); return Math.Sin(x * Math.PI / 2);

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e63a2384482cb7b418bc1a4149b11742
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,31 @@
using UnityEngine.EventSystems;
namespace AlicizaX.UI
{
internal interface IItemInteractionHost
{
ItemInteractionFlags InteractionFlags { get; }
void HandlePointerClick(PointerEventData eventData);
void HandlePointerEnter(PointerEventData eventData);
void HandlePointerExit(PointerEventData eventData);
void HandleSelect(BaseEventData eventData);
void HandleDeselect(BaseEventData eventData);
void HandleMove(AxisEventData eventData);
void HandleBeginDrag(PointerEventData eventData);
void HandleDrag(PointerEventData eventData);
void HandleEndDrag(PointerEventData eventData);
void HandleSubmit(BaseEventData eventData);
void HandleCancel(BaseEventData eventData);
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: b174ccb64b3938c449d4a69a3262d8d5 guid: b993c99fa9bf9634a8eb949a82efe103
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@ -0,0 +1,21 @@
using System;
namespace AlicizaX.UI
{
[Flags]
public enum ItemInteractionFlags
{
None = 0,
PointerClick = 1 << 0,
PointerEnter = 1 << 1,
PointerExit = 1 << 2,
Select = 1 << 3,
Deselect = 1 << 4,
Move = 1 << 5,
BeginDrag = 1 << 6,
Drag = 1 << 7,
EndDrag = 1 << 8,
Submit = 1 << 9,
Cancel = 1 << 10,
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 491a09d781095104989abb7a91424008 guid: 264e45e52d936c44b96c6bb5eeaf4b98
MonoImporter: MonoImporter:
externalObjects: {} externalObjects: {}
serializedVersion: 2 serializedVersion: 2

View File

@ -0,0 +1,205 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AlicizaX.UI
{
[DisallowMultipleComponent]
public sealed class ItemInteractionProxy : MonoBehaviour,
IPointerClickHandler,
IPointerEnterHandler,
IPointerExitHandler,
ISelectHandler,
IDeselectHandler,
IMoveHandler,
IBeginDragHandler,
IDragHandler,
IEndDragHandler,
ISubmitHandler,
ICancelHandler
{
private IItemInteractionHost host;
private ItemInteractionFlags flags;
private Selectable focusAnchor;
private RecyclerItemSelectable ownedSelectable;
private Scroller parentScroller;
internal void Bind(IItemInteractionHost interactionHost)
{
host = interactionHost;
flags = interactionHost?.InteractionFlags ?? ItemInteractionFlags.None;
parentScroller = GetComponentInParent<Scroller>();
EnsureFocusAnchor();
if (ownedSelectable != null)
{
bool requiresSelection = RequiresSelection(flags);
ownedSelectable.interactable = requiresSelection;
ownedSelectable.enabled = requiresSelection;
}
InvalidateNavigationScope();
}
public void Clear()
{
host = null;
flags = ItemInteractionFlags.None;
parentScroller = null;
if (ownedSelectable != null)
{
ownedSelectable.interactable = false;
ownedSelectable.enabled = false;
}
InvalidateNavigationScope();
}
public Selectable GetSelectable()
{
EnsureFocusAnchor();
return focusAnchor;
}
public void OnPointerClick(PointerEventData eventData)
{
if ((flags & ItemInteractionFlags.PointerClick) != 0)
{
host?.HandlePointerClick(eventData);
}
}
public void OnPointerEnter(PointerEventData eventData)
{
if ((flags & ItemInteractionFlags.PointerEnter) != 0)
{
host?.HandlePointerEnter(eventData);
}
}
public void OnPointerExit(PointerEventData eventData)
{
if ((flags & ItemInteractionFlags.PointerExit) != 0)
{
host?.HandlePointerExit(eventData);
}
}
public void OnSelect(BaseEventData eventData)
{
if ((flags & ItemInteractionFlags.Select) != 0)
{
host?.HandleSelect(eventData);
}
}
public void OnDeselect(BaseEventData eventData)
{
if ((flags & ItemInteractionFlags.Deselect) != 0)
{
host?.HandleDeselect(eventData);
}
}
public void OnMove(AxisEventData eventData)
{
if ((flags & ItemInteractionFlags.Move) != 0)
{
host?.HandleMove(eventData);
}
}
public void OnBeginDrag(PointerEventData eventData)
{
if ((flags & ItemInteractionFlags.BeginDrag) != 0)
{
host?.HandleBeginDrag(eventData);
return;
}
parentScroller?.OnBeginDrag(eventData);
}
public void OnDrag(PointerEventData eventData)
{
if ((flags & ItemInteractionFlags.Drag) != 0)
{
host?.HandleDrag(eventData);
return;
}
parentScroller?.OnDrag(eventData);
}
public void OnEndDrag(PointerEventData eventData)
{
if ((flags & ItemInteractionFlags.EndDrag) != 0)
{
host?.HandleEndDrag(eventData);
return;
}
parentScroller?.OnEndDrag(eventData);
}
public void OnSubmit(BaseEventData eventData)
{
if ((flags & ItemInteractionFlags.Submit) != 0)
{
host?.HandleSubmit(eventData);
}
}
public void OnCancel(BaseEventData eventData)
{
if ((flags & ItemInteractionFlags.Cancel) != 0)
{
host?.HandleCancel(eventData);
}
}
private void EnsureFocusAnchor()
{
if (focusAnchor != null)
{
return;
}
focusAnchor = GetComponent<Selectable>();
if (focusAnchor != null)
{
return;
}
ownedSelectable = GetComponent<RecyclerItemSelectable>();
if (ownedSelectable == null)
{
ownedSelectable = gameObject.AddComponent<RecyclerItemSelectable>();
}
focusAnchor = ownedSelectable;
}
private static bool RequiresSelection(ItemInteractionFlags interactionFlags)
{
const ItemInteractionFlags selectionFlags =
ItemInteractionFlags.Select |
ItemInteractionFlags.Deselect |
ItemInteractionFlags.Move |
ItemInteractionFlags.Submit |
ItemInteractionFlags.Cancel;
return (interactionFlags & selectionFlags) != 0;
}
[System.Diagnostics.Conditional("INPUTSYSTEM_SUPPORT")]
private void InvalidateNavigationScope()
{
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
var scope = GetComponentInParent<UnityEngine.UI.UXNavigationScope>(true);
scope?.InvalidateSelectableCache();
#endif
}
}
}

View File

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

View File

@ -0,0 +1,16 @@
using UnityEngine.UI;
namespace AlicizaX.UI
{
public sealed class RecyclerItemSelectable : Selectable
{
protected override void Awake()
{
base.Awake();
transition = Transition.None;
Navigation disabledNavigation = navigation;
disabledNavigation.mode = Navigation.Mode.None;
navigation = disabledNavigation;
}
}
}

View File

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

View File

@ -1,4 +1,3 @@
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI namespace AlicizaX.UI
@ -21,9 +20,17 @@ namespace AlicizaX.UI
public override Vector2 CalculateContentSize() public override Vector2 CalculateContentSize()
{ {
int itemCount = adapter != null ? adapter.GetItemCount() : 0;
if (itemCount <= 0)
{
radius = 0f;
intervalAngle = 0f;
return viewportSize;
}
Vector2 size = viewProvider.CalculateViewSize(0); 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); 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; intervalAngle = 360f / itemCount;
return viewportSize; return viewportSize;
} }
@ -62,7 +69,7 @@ namespace AlicizaX.UI
public override int GetEndIndex() public override int GetEndIndex()
{ {
return adapter.GetItemCount() - 1; return adapter == null || adapter.GetItemCount() <= 0 ? -1 : adapter.GetItemCount() - 1;
} }
public override bool IsFullVisibleStart(int index) => false; public override bool IsFullVisibleStart(int index) => false;
@ -77,6 +84,11 @@ namespace AlicizaX.UI
public override float IndexToPosition(int index) public override float IndexToPosition(int index)
{ {
if (Mathf.Approximately(intervalAngle, 0f))
{
return 0f;
}
float position = index * intervalAngle; float position = index * intervalAngle;
return -position; return -position;
@ -84,13 +96,18 @@ namespace AlicizaX.UI
public override int PositionToIndex(float position) public override int PositionToIndex(float position)
{ {
if (Mathf.Approximately(intervalAngle, 0f))
{
return 0;
}
int index = Mathf.RoundToInt(position / intervalAngle); int index = Mathf.RoundToInt(position / intervalAngle);
return -index; return -index;
} }
public override void DoItemAnimation() public override void DoItemAnimation()
{ {
List<ViewHolder> viewHolders = viewProvider.ViewHolders; var viewHolders = viewProvider.ViewHolders;
for (int i = 0; i < viewHolders.Count; i++) for (int i = 0; i < viewHolders.Count; i++)
{ {
float angle = i * intervalAngle + initalAngle; float angle = i * intervalAngle + initalAngle;

View File

@ -1,4 +1,5 @@
using UnityEngine; using UnityEngine;
using UnityEngine.Serialization;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
@ -7,18 +8,28 @@ namespace AlicizaX.UI
{ {
private Vector2 cellSize; private Vector2 cellSize;
[SerializeField] private int cellCounnt = 1; [FormerlySerializedAs("cellCounnt")]
[SerializeField] private int cellCount = 1;
public GridLayoutManager() public GridLayoutManager()
{ {
this.unit = cellCounnt; unit = cellCount;
} }
public override Vector2 CalculateContentSize() public override Vector2 CalculateContentSize()
{ {
int itemCount = adapter != null ? adapter.GetItemCount() : 0;
if (itemCount <= 0)
{
cellSize = Vector2.zero;
return direction == Direction.Vertical
? new Vector2(contentSize.x, padding.y * 2)
: new Vector2(padding.x * 2, contentSize.y);
}
cellSize = viewProvider.CalculateViewSize(0); cellSize = viewProvider.CalculateViewSize(0);
int row = Mathf.CeilToInt(adapter.GetItemCount() / (float)unit); int row = Mathf.CeilToInt(itemCount / (float)unit);
float len; float len;
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
{ {
@ -51,6 +62,11 @@ namespace AlicizaX.UI
public override Vector2 CalculateContentOffset() public override Vector2 CalculateContentOffset()
{ {
if (cellSize == Vector2.zero)
{
return Vector2.zero;
}
float width, height; float width, height;
if (alignment == Alignment.Center) if (alignment == Alignment.Center)
{ {
@ -67,6 +83,11 @@ namespace AlicizaX.UI
public override Vector2 CalculateViewportOffset() public override Vector2 CalculateViewportOffset()
{ {
if (cellSize == Vector2.zero)
{
return Vector2.zero;
}
float width, height; float width, height;
if (alignment == Alignment.Center) if (alignment == Alignment.Center)
{ {
@ -83,21 +104,46 @@ namespace AlicizaX.UI
public override int GetStartIndex() public override int GetStartIndex()
{ {
if (adapter == null || adapter.GetItemCount() <= 0)
{
return 0;
}
float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x; float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x;
if (len <= 0f)
{
return 0;
}
int index = Mathf.FloorToInt(ScrollPosition / len) * unit; int index = Mathf.FloorToInt(ScrollPosition / len) * unit;
return Mathf.Max(0, index); return Mathf.Max(0, index);
} }
public override int GetEndIndex() public override int GetEndIndex()
{ {
if (adapter == null || adapter.GetItemCount() <= 0)
{
return -1;
}
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x; float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x;
if (len <= 0f)
{
return adapter.GetItemCount() - 1;
}
int index = Mathf.FloorToInt((ScrollPosition + viewLength) / len) * unit; int index = Mathf.FloorToInt((ScrollPosition + viewLength) / len) * unit;
return Mathf.Min(index, adapter.GetItemCount() - 1); return Mathf.Min(index, adapter.GetItemCount() - 1);
} }
public override float IndexToPosition(int index) public override float IndexToPosition(int index)
{ {
if (adapter == null || adapter.GetItemCount() <= 0)
{
return 0f;
}
int row = index / unit; int row = index / unit;
float len, viewLength, position; float len, viewLength, position;
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
@ -119,6 +165,11 @@ namespace AlicizaX.UI
public override int PositionToIndex(float position) public override int PositionToIndex(float position)
{ {
float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x; float len = direction == Direction.Vertical ? cellSize.y + spacing.y : cellSize.x + spacing.x;
if (len <= 0f)
{
return 0;
}
int index = Mathf.RoundToInt(position / len); int index = Mathf.RoundToInt(position / len);
return index * unit; return index * unit;

View File

@ -4,112 +4,38 @@ namespace AlicizaX.UI
{ {
public interface ILayoutManager public interface ILayoutManager
{ {
/// <summary>
/// 滚动时,刷新整个页面的布局
/// </summary>
void UpdateLayout(); void UpdateLayout();
/// <summary>
/// 为 ViewHolder 设置布局
/// </summary>
/// <param name="viewHolder"></param>
/// <param name="index"></param>
void Layout(ViewHolder viewHolder, int index); void Layout(ViewHolder viewHolder, int index);
/// <summary>
/// 设置 Content 大小
/// </summary>
void SetContentSize(); void SetContentSize();
/// <summary>
/// 计算 Content 的大小
/// </summary>
/// <returns></returns>
Vector2 CalculateContentSize(); Vector2 CalculateContentSize();
/// <summary>
/// 计算第 index 个 ViewHolder 到顶部的距离
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
Vector2 CalculatePosition(int index); Vector2 CalculatePosition(int index);
/// <summary>
/// 计算 ViewHolder 相对于内容长度的偏移
/// </summary>
/// <returns></returns>
Vector2 CalculateContentOffset(); Vector2 CalculateContentOffset();
/// <summary>
/// 计算 ViewHolder 相对于视口的偏移
/// </summary>
/// <returns></returns>
Vector2 CalculateViewportOffset(); Vector2 CalculateViewportOffset();
/// <summary>
/// 获取当前显示的第一个 ViewHolder 下标
/// </summary>
/// <returns></returns>
int GetStartIndex(); int GetStartIndex();
/// <summary>
/// 获取当前显示的最后一个 ViewHolder 下标
/// </summary>
/// <returns></returns>
int GetEndIndex(); int GetEndIndex();
/// <summary>
/// 数据下标转换成在布局中对应的位置
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
float IndexToPosition(int index); float IndexToPosition(int index);
/// <summary>
/// 在布局中的位置转换成数据下标
/// </summary>
/// <param name="position"></param>
/// <returns></returns>
int PositionToIndex(float position); int PositionToIndex(float position);
/// <summary>
/// 滚动时item 对应的动画
/// </summary>
void DoItemAnimation(); void DoItemAnimation();
/// <summary>
/// 判断第一个 ViewHolder 是否完全可见
/// </summary>
/// <param name="index">数据的真实下标</param>
/// <returns></returns>
bool IsFullVisibleStart(int index); bool IsFullVisibleStart(int index);
/// <summary>
/// 判断第一个 ViewHolder 是否完全不可见
/// </summary>
/// <param name="index">数据的真实下标</param>
/// <returns></returns>
bool IsFullInvisibleStart(int index); bool IsFullInvisibleStart(int index);
/// <summary>
/// 判定最后一个 ViewHolder 是否完全可见
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
bool IsFullVisibleEnd(int index); bool IsFullVisibleEnd(int index);
/// <summary>
/// 判定最后一个 ViewHolder 是否完全不可见
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
bool IsFullInvisibleEnd(int index); bool IsFullInvisibleEnd(int index);
/// <summary>
/// 判定第 index ViewHolder是否可见
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
bool IsVisible(int index); bool IsVisible(int index);
} }
} }

View File

@ -1,19 +1,11 @@
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
/// <summary>
/// 布局管理器抽象基类
/// 负责计算和管理 RecyclerView 中列表项的位置、大小和可见性
/// 子类需要实现具体的布局算法(如线性、网格、圆形等)
/// </summary>
[System.Serializable] [System.Serializable]
public abstract class LayoutManager : ILayoutManager public abstract class LayoutManager : ILayoutManager
{ {
protected Vector2 viewportSize; protected Vector2 viewportSize;
/// <summary>
/// 获取视口大小(可见区域的尺寸)
/// </summary>
public Vector2 ViewportSize public Vector2 ViewportSize
{ {
get => viewportSize; get => viewportSize;
@ -21,9 +13,6 @@ namespace AlicizaX.UI
} }
protected Vector2 contentSize; protected Vector2 contentSize;
/// <summary>
/// 获取内容总大小(所有列表项占据的总尺寸)
/// </summary>
public Vector2 ContentSize public Vector2 ContentSize
{ {
get => contentSize; get => contentSize;
@ -31,9 +20,6 @@ namespace AlicizaX.UI
} }
protected Vector2 contentOffset; protected Vector2 contentOffset;
/// <summary>
/// 获取内容偏移量(用于对齐计算)
/// </summary>
public Vector2 ContentOffset public Vector2 ContentOffset
{ {
get => contentOffset; get => contentOffset;
@ -41,9 +27,6 @@ namespace AlicizaX.UI
} }
protected Vector2 viewportOffset; protected Vector2 viewportOffset;
/// <summary>
/// 获取视口偏移量(用于对齐计算)
/// </summary>
public Vector2 ViewportOffset public Vector2 ViewportOffset
{ {
get => viewportOffset; get => viewportOffset;
@ -51,9 +34,6 @@ namespace AlicizaX.UI
} }
protected IAdapter adapter; protected IAdapter adapter;
/// <summary>
/// 获取或设置数据适配器
/// </summary>
public IAdapter Adapter public IAdapter Adapter
{ {
get => adapter; get => adapter;
@ -61,9 +41,6 @@ namespace AlicizaX.UI
} }
protected ViewProvider viewProvider; protected ViewProvider viewProvider;
/// <summary>
/// 获取或设置视图提供器
/// </summary>
public ViewProvider ViewProvider public ViewProvider ViewProvider
{ {
get => viewProvider; get => viewProvider;
@ -71,9 +48,6 @@ namespace AlicizaX.UI
} }
protected RecyclerView recyclerView; protected RecyclerView recyclerView;
/// <summary>
/// 获取或设置关联的 RecyclerView 实例
/// </summary>
public virtual RecyclerView RecyclerView public virtual RecyclerView RecyclerView
{ {
get => recyclerView; get => recyclerView;
@ -81,9 +55,6 @@ namespace AlicizaX.UI
} }
protected Direction direction; protected Direction direction;
/// <summary>
/// 获取或设置滚动方向
/// </summary>
public Direction Direction public Direction Direction
{ {
get => direction; get => direction;
@ -91,9 +62,6 @@ namespace AlicizaX.UI
} }
protected Alignment alignment; protected Alignment alignment;
/// <summary>
/// 获取或设置对齐方式
/// </summary>
public Alignment Alignment public Alignment Alignment
{ {
get => alignment; get => alignment;
@ -101,9 +69,6 @@ namespace AlicizaX.UI
} }
protected Vector2 spacing; protected Vector2 spacing;
/// <summary>
/// 获取或设置列表项间距
/// </summary>
public Vector2 Spacing public Vector2 Spacing
{ {
get => spacing; get => spacing;
@ -111,9 +76,6 @@ namespace AlicizaX.UI
} }
protected Vector2 padding; protected Vector2 padding;
/// <summary>
/// 获取或设置内边距
/// </summary>
public Vector2 Padding public Vector2 Padding
{ {
get => padding; get => padding;
@ -121,9 +83,6 @@ namespace AlicizaX.UI
} }
protected int unit = 1; protected int unit = 1;
/// <summary>
/// 获取或设置布局单元(用于网格布局等,表示一次处理多少个项)
/// </summary>
public int Unit public int Unit
{ {
get => unit; get => unit;
@ -131,17 +90,10 @@ namespace AlicizaX.UI
} }
/// <summary>
/// 获取当前滚动位置
/// </summary>
public float ScrollPosition => recyclerView.GetScrollPosition(); public float ScrollPosition => recyclerView.GetScrollPosition();
public LayoutManager() { } public LayoutManager() { }
/// <summary>
/// 设置内容大小
/// 计算视口大小、内容大小以及各种偏移量
/// </summary>
public void SetContentSize() public void SetContentSize()
{ {
viewportSize = recyclerView.GetComponent<RectTransform>().rect.size; viewportSize = recyclerView.GetComponent<RectTransform>().rect.size;
@ -150,10 +102,6 @@ namespace AlicizaX.UI
viewportOffset = CalculateViewportOffset(); viewportOffset = CalculateViewportOffset();
} }
/// <summary>
/// 更新所有可见 ViewHolder 的布局
/// 遍历所有当前显示的 ViewHolder 并重新计算其位置
/// </summary>
public void UpdateLayout() public void UpdateLayout()
{ {
foreach (var viewHolder in viewProvider.ViewHolders) foreach (var viewHolder in viewProvider.ViewHolders)
@ -162,11 +110,6 @@ namespace AlicizaX.UI
} }
} }
/// <summary>
/// 为指定的 ViewHolder 设置布局位置
/// </summary>
/// <param name="viewHolder">要布局的 ViewHolder</param>
/// <param name="index">ViewHolder 对应的数据索引</param>
public virtual void Layout(ViewHolder viewHolder, int index) public virtual void Layout(ViewHolder viewHolder, int index)
{ {
Vector2 pos = CalculatePosition(index); Vector2 pos = CalculatePosition(index);
@ -176,67 +119,24 @@ namespace AlicizaX.UI
viewHolder.RectTransform.anchoredPosition3D = position; viewHolder.RectTransform.anchoredPosition3D = position;
} }
/// <summary>
/// 计算内容总大小(抽象方法,由子类实现)
/// </summary>
/// <returns>内容的总尺寸</returns>
public abstract Vector2 CalculateContentSize(); public abstract Vector2 CalculateContentSize();
/// <summary>
/// 计算指定索引的 ViewHolder 位置(抽象方法,由子类实现)
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>ViewHolder 的位置</returns>
public abstract Vector2 CalculatePosition(int index); public abstract Vector2 CalculatePosition(int index);
/// <summary>
/// 计算内容偏移量(抽象方法,由子类实现)
/// </summary>
/// <returns>内容偏移量</returns>
public abstract Vector2 CalculateContentOffset(); public abstract Vector2 CalculateContentOffset();
/// <summary>
/// 计算视口偏移量(抽象方法,由子类实现)
/// </summary>
/// <returns>视口偏移量</returns>
public abstract Vector2 CalculateViewportOffset(); public abstract Vector2 CalculateViewportOffset();
/// <summary>
/// 获取当前可见区域的起始索引(抽象方法,由子类实现)
/// </summary>
/// <returns>起始索引</returns>
public abstract int GetStartIndex(); public abstract int GetStartIndex();
/// <summary>
/// 获取当前可见区域的结束索引(抽象方法,由子类实现)
/// </summary>
/// <returns>结束索引</returns>
public abstract int GetEndIndex(); public abstract int GetEndIndex();
/// <summary>
/// 将数据索引转换为滚动位置(抽象方法,由子类实现)
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>对应的滚动位置</returns>
public abstract float IndexToPosition(int index); public abstract float IndexToPosition(int index);
/// <summary>
/// 将滚动位置转换为数据索引(抽象方法,由子类实现)
/// </summary>
/// <param name="position">滚动位置</param>
/// <returns>对应的数据索引</returns>
public abstract int PositionToIndex(float position); public abstract int PositionToIndex(float position);
/// <summary>
/// 执行列表项动画(虚方法,子类可选择性重写)
/// </summary>
public virtual void DoItemAnimation() { } public virtual void DoItemAnimation() { }
/// <summary>
/// 判断起始位置的 ViewHolder 是否完全可见
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>如果完全可见返回 true否则返回 false</returns>
public virtual bool IsFullVisibleStart(int index) public virtual bool IsFullVisibleStart(int index)
{ {
Vector2 vector2 = CalculatePosition(index); Vector2 vector2 = CalculatePosition(index);
@ -244,11 +144,6 @@ namespace AlicizaX.UI
return position + GetOffset() >= 0; return position + GetOffset() >= 0;
} }
/// <summary>
/// 判断起始位置的 ViewHolder 是否完全不可见
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>如果完全不可见返回 true否则返回 false</returns>
public virtual bool IsFullInvisibleStart(int index) public virtual bool IsFullInvisibleStart(int index)
{ {
Vector2 vector2 = CalculatePosition(index + unit); Vector2 vector2 = CalculatePosition(index + unit);
@ -256,11 +151,6 @@ namespace AlicizaX.UI
return position + GetOffset() < 0; return position + GetOffset() < 0;
} }
/// <summary>
/// 判断结束位置的 ViewHolder 是否完全可见
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>如果完全可见返回 true否则返回 false</returns>
public virtual bool IsFullVisibleEnd(int index) public virtual bool IsFullVisibleEnd(int index)
{ {
Vector2 vector2 = CalculatePosition(index + unit); Vector2 vector2 = CalculatePosition(index + unit);
@ -269,11 +159,6 @@ namespace AlicizaX.UI
return position + GetOffset() <= viewLength; return position + GetOffset() <= viewLength;
} }
/// <summary>
/// 判断结束位置的 ViewHolder 是否完全不可见
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>如果完全不可见返回 true否则返回 false</returns>
public virtual bool IsFullInvisibleEnd(int index) public virtual bool IsFullInvisibleEnd(int index)
{ {
Vector2 vector2 = CalculatePosition(index); Vector2 vector2 = CalculatePosition(index);
@ -282,11 +167,6 @@ namespace AlicizaX.UI
return position + GetOffset() > viewLength; return position + GetOffset() > viewLength;
} }
/// <summary>
/// 判断指定索引的 ViewHolder 是否可见(部分或完全)
/// </summary>
/// <param name="index">数据索引</param>
/// <returns>如果可见返回 true否则返回 false</returns>
public virtual bool IsVisible(int index) public virtual bool IsVisible(int index)
{ {
float position, viewLength; float position, viewLength;
@ -309,11 +189,6 @@ namespace AlicizaX.UI
return false; return false;
} }
/// <summary>
/// 获取适配内容大小
/// 根据对齐方式计算实际显示的内容长度
/// </summary>
/// <returns>适配后的内容大小</returns>
protected virtual float GetFitContentSize() protected virtual float GetFitContentSize()
{ {
float len; float len;
@ -328,40 +203,23 @@ namespace AlicizaX.UI
return len; return len;
} }
/// <summary>
/// 获取偏移量
/// 计算内容偏移和视口偏移的组合值
/// </summary>
/// <returns>总偏移量</returns>
protected virtual float GetOffset() protected virtual float GetOffset()
{ {
return direction == Direction.Vertical ? -contentOffset.y + viewportOffset.y : -contentOffset.x + viewportOffset.x; return direction == Direction.Vertical ? -contentOffset.y + viewportOffset.y : -contentOffset.x + viewportOffset.x;
} }
} }
/// <summary>
/// 滚动方向枚举
/// </summary>
public enum Direction public enum Direction
{ {
/// <summary>垂直滚动</summary>
Vertical = 0, Vertical = 0,
/// <summary>水平滚动</summary>
Horizontal = 1, Horizontal = 1,
/// <summary>自定义滚动</summary>
Custom = 2 Custom = 2
} }
/// <summary>
/// 对齐方式枚举
/// </summary>
public enum Alignment public enum Alignment
{ {
/// <summary>左对齐</summary>
Left, Left,
/// <summary>居中对齐</summary>
Center, Center,
/// <summary>顶部对齐</summary>
Top Top
} }
} }

View File

@ -12,17 +12,25 @@ namespace AlicizaX.UI
public override Vector2 CalculateContentSize() public override Vector2 CalculateContentSize()
{ {
int itemCount = adapter != null ? adapter.GetItemCount() : 0;
if (itemCount <= 0)
{
lineHeight = 0f;
return direction == Direction.Vertical
? new Vector2(contentSize.x, padding.y * 2)
: new Vector2(padding.x * 2, contentSize.y);
}
Vector2 size = viewProvider.CalculateViewSize(0); Vector2 size = viewProvider.CalculateViewSize(0);
lineHeight = direction == Direction.Vertical ? size.y : size.x; lineHeight = direction == Direction.Vertical ? size.y : size.x;
int index = adapter.GetItemCount();
float position; float position;
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
{ {
position = index * (lineHeight + spacing.y) - spacing.y; position = itemCount * (lineHeight + spacing.y) - spacing.y;
return new Vector2(contentSize.x, position + padding.y * 2); return new Vector2(contentSize.x, position + padding.y * 2);
} }
position = index * (lineHeight + spacing.x) - spacing.x; position = itemCount * (lineHeight + spacing.x) - spacing.x;
return new Vector2(position + padding.x * 2, contentSize.y); return new Vector2(position + padding.x * 2, contentSize.y);
} }
@ -40,6 +48,11 @@ namespace AlicizaX.UI
public override Vector2 CalculateContentOffset() public override Vector2 CalculateContentOffset()
{ {
if (lineHeight <= 0f)
{
return Vector2.zero;
}
float len = GetFitContentSize(); float len = GetFitContentSize();
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
{ {
@ -50,6 +63,11 @@ namespace AlicizaX.UI
public override Vector2 CalculateViewportOffset() public override Vector2 CalculateViewportOffset()
{ {
if (lineHeight <= 0f)
{
return Vector2.zero;
}
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
{ {
return new Vector2(0, (viewportSize.y - lineHeight) / 2); return new Vector2(0, (viewportSize.y - lineHeight) / 2);
@ -59,22 +77,42 @@ namespace AlicizaX.UI
public override int GetStartIndex() public override int GetStartIndex()
{ {
if (adapter == null || adapter.GetItemCount() <= 0)
{
return 0;
}
float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x;
if (len <= 0f)
{
return 0;
}
int index = Mathf.FloorToInt(ScrollPosition / len); int index = Mathf.FloorToInt(ScrollPosition / len);
return Mathf.Max(0, index); return Mathf.Max(0, index);
} }
public override int GetEndIndex() public override int GetEndIndex()
{ {
if (adapter == null || adapter.GetItemCount() <= 0)
{
return -1;
}
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x;
if (len <= 0f)
{
return adapter.GetItemCount() - 1;
}
int index = Mathf.FloorToInt((ScrollPosition + viewLength) / len); int index = Mathf.FloorToInt((ScrollPosition + viewLength) / len);
return Mathf.Min(index, adapter.GetItemCount() - 1); return Mathf.Min(index, adapter.GetItemCount() - 1);
} }
public override float IndexToPosition(int index) public override float IndexToPosition(int index)
{ {
if (index < 0 || index >= adapter.GetItemCount()) return 0; if (adapter == null || adapter.GetItemCount() <= 0 || index < 0 || index >= adapter.GetItemCount()) return 0;
float len, viewLength, position; float len, viewLength, position;
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
@ -96,6 +134,11 @@ namespace AlicizaX.UI
public override int PositionToIndex(float position) public override int PositionToIndex(float position)
{ {
float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x;
if (len <= 0f)
{
return 0;
}
int index = Mathf.RoundToInt(position / len); int index = Mathf.RoundToInt(position / len);
return index; return index;

View File

@ -6,133 +6,238 @@ namespace AlicizaX.UI
[Serializable] [Serializable]
public class MixedLayoutManager : LayoutManager public class MixedLayoutManager : LayoutManager
{ {
private float[] itemLengths = Array.Empty<float>();
private float[] itemPositions = Array.Empty<float>();
private Vector2 firstItemSize = Vector2.zero;
private int cachedItemCount = -1;
private bool positionCacheDirty = true;
public MixedLayoutManager() { } public MixedLayoutManager() { }
public override Vector2 CalculateContentSize() public override Vector2 CalculateContentSize()
{ {
int index = adapter.GetItemCount(); positionCacheDirty = true;
float position = 0; EnsurePositionCache();
for (int i = 0; i < index; i++)
{
position += GetLength(i);
}
return direction == Direction.Vertical ? float totalLength = cachedItemCount > 0
new Vector2(contentSize.x, position - spacing.y + padding.y * 2) : ? itemPositions[cachedItemCount - 1] + itemLengths[cachedItemCount - 1]
new Vector2(position - spacing.x + padding.x * 2, contentSize.y); : 0f;
float paddingLength = direction == Direction.Vertical ? padding.y * 2 : padding.x * 2;
return direction == Direction.Vertical
? new Vector2(contentSize.x, cachedItemCount > 0 ? totalLength + paddingLength : paddingLength)
: new Vector2(cachedItemCount > 0 ? totalLength + paddingLength : paddingLength, contentSize.y);
} }
public override Vector2 CalculatePosition(int index) public override Vector2 CalculatePosition(int index)
{ {
// TODO 优化点,将 position 定义成全局变量 EnsurePositionCache();
float position = 0;
for (int i = 0; i < index; i++) float position = GetItemPosition(index) - ScrollPosition;
{ return direction == Direction.Vertical
position += GetLength(i); ? new Vector2(0, position + padding.y)
} : new Vector2(position + padding.x, 0);
position -= ScrollPosition;
return direction == Direction.Vertical ? new Vector2(0, position + padding.y) : new Vector2(position + padding.x, 0);
} }
public override Vector2 CalculateContentOffset() public override Vector2 CalculateContentOffset()
{ {
Vector2 size = viewProvider.CalculateViewSize(0); EnsurePositionCache();
if (cachedItemCount <= 0)
{
return Vector2.zero;
}
float len = GetFitContentSize(); float len = GetFitContentSize();
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
{ {
return new Vector2(0, (len - size.y) / 2); return new Vector2(0, (len - firstItemSize.y) / 2);
} }
return new Vector2((len - size.x) / 2, 0);
return new Vector2((len - firstItemSize.x) / 2, 0);
} }
public override Vector2 CalculateViewportOffset() public override Vector2 CalculateViewportOffset()
{ {
Vector2 size = viewProvider.CalculateViewSize(0); EnsurePositionCache();
if (cachedItemCount <= 0)
{
return Vector2.zero;
}
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
{ {
return new Vector2(0, (viewportSize.y - size.y) / 2); return new Vector2(0, (viewportSize.y - firstItemSize.y) / 2);
} }
return new Vector2((viewportSize.x - size.x) / 2, 0);
return new Vector2((viewportSize.x - firstItemSize.x) / 2, 0);
} }
public override int GetStartIndex() public override int GetStartIndex()
{ {
float position = 0; EnsurePositionCache();
float contentPosition = ScrollPosition; if (cachedItemCount <= 0)
int itemCount = adapter.GetItemCount();
for (int i = 0; i < itemCount; i++)
{ {
position += GetLength(i);
if (position > contentPosition)
{
return Mathf.Max(0, i);
}
}
return 0; return 0;
} }
int index = FindFirstItemEndingAfter(ScrollPosition);
return index >= 0 ? index : 0;
}
public override int GetEndIndex() public override int GetEndIndex()
{ {
float position = 0; EnsurePositionCache();
if (cachedItemCount <= 0)
{
return -1;
}
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x; float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
int itemCount = adapter.GetItemCount(); int index = FindFirstItemEndingAfter(ScrollPosition + viewLength);
for (int i = 0; i < itemCount; i++) return index >= 0 ? Mathf.Min(index, cachedItemCount - 1) : cachedItemCount - 1;
{
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) public override float IndexToPosition(int index)
{ {
Vector2 position = CalculatePosition(index); EnsurePositionCache();
if (cachedItemCount <= 0)
{
return 0f;
}
float position = GetItemPosition(index);
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
{ {
position.y = Mathf.Max(0, position.y); return Mathf.Clamp(position, 0f, Mathf.Max(contentSize.y - viewportSize.y, 0f));
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;
} }
return Mathf.Clamp(position, 0f, Mathf.Max(contentSize.x - viewportSize.x, 0f));
} }
public override int PositionToIndex(float position) public override int PositionToIndex(float position)
{ {
float len = 0; EnsurePositionCache();
if (cachedItemCount <= 0)
{
return 0;
}
int itemCount = adapter.GetItemCount(); int index = FindFirstItemEndingAtOrAfter(position);
return index >= 0 ? index : cachedItemCount - 1;
}
private void EnsurePositionCache()
{
int itemCount = adapter != null ? adapter.GetItemCount() : 0;
if (itemCount < 0)
{
itemCount = 0;
}
if (!positionCacheDirty && cachedItemCount == itemCount)
{
return;
}
RebuildPositionCache(itemCount);
}
private void RebuildPositionCache(int itemCount)
{
if (itemLengths.Length != itemCount)
{
itemLengths = itemCount > 0 ? new float[itemCount] : Array.Empty<float>();
itemPositions = itemCount > 0 ? new float[itemCount] : Array.Empty<float>();
}
firstItemSize = itemCount > 0 ? viewProvider.CalculateViewSize(0) : Vector2.zero;
float position = 0f;
for (int i = 0; i < itemCount; i++) for (int i = 0; i < itemCount; i++)
{ {
len += GetLength(i); itemPositions[i] = position;
itemLengths[i] = GetLength(i, itemCount);
position += itemLengths[i];
}
if (len >= position) cachedItemCount = itemCount;
positionCacheDirty = false;
}
private float GetItemPosition(int index)
{ {
return i; if (index <= 0 || cachedItemCount <= 0)
{
return 0f;
}
if (index >= cachedItemCount)
{
return itemPositions[cachedItemCount - 1] + itemLengths[cachedItemCount - 1];
}
return itemPositions[index];
}
private int FindFirstItemEndingAfter(float position)
{
int low = 0;
int high = cachedItemCount - 1;
int result = -1;
while (low <= high)
{
int mid = low + ((high - low) / 2);
if (GetItemEndPosition(mid) > position)
{
result = mid;
high = mid - 1;
}
else
{
low = mid + 1;
} }
} }
return 0; return result;
}
private int FindFirstItemEndingAtOrAfter(float position)
{
int low = 0;
int high = cachedItemCount - 1;
int result = -1;
while (low <= high)
{
int mid = low + ((high - low) / 2);
if (GetItemEndPosition(mid) >= position)
{
result = mid;
high = mid - 1;
}
else
{
low = mid + 1;
}
}
return result;
}
private float GetItemEndPosition(int index)
{
return itemPositions[index] + itemLengths[index];
}
private float GetLength(int index, int itemCount)
{
Vector2 size = viewProvider.CalculateViewSize(index);
if (index < itemCount - 1)
{
size += spacing;
}
return direction == Direction.Vertical ? size.y : size.x;
} }
} }
} }

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI namespace AlicizaX.UI
@ -15,19 +14,27 @@ namespace AlicizaX.UI
public override Vector2 CalculateContentSize() public override Vector2 CalculateContentSize()
{ {
int itemCount = adapter != null ? adapter.GetItemCount() : 0;
if (itemCount <= 0)
{
lineHeight = 0f;
return direction == Direction.Vertical
? new Vector2(contentSize.x, padding.y * 2)
: new Vector2(padding.x * 2, contentSize.y);
}
Vector2 size = viewProvider.CalculateViewSize(0); Vector2 size = viewProvider.CalculateViewSize(0);
lineHeight = direction == Direction.Vertical ? size.y : size.x; lineHeight = direction == Direction.Vertical ? size.y : size.x;
int index = adapter.GetItemCount();
float position; float position;
if (direction == Direction.Vertical) if (direction == Direction.Vertical)
{ {
position = index * (lineHeight + spacing.y) - spacing.y; position = itemCount * (lineHeight + spacing.y) - spacing.y;
position += viewportSize.y - lineHeight; position += viewportSize.y - lineHeight;
return new Vector2(contentSize.x, position + padding.y * 2); return new Vector2(contentSize.x, position + padding.y * 2);
} }
position = index * (lineHeight + spacing.x) - spacing.x; position = itemCount * (lineHeight + spacing.x) - spacing.x;
position += viewportSize.x - lineHeight; position += viewportSize.x - lineHeight;
return new Vector2(position + padding.x * 2, contentSize.y); return new Vector2(position + padding.x * 2, contentSize.y);
} }
@ -57,15 +64,29 @@ namespace AlicizaX.UI
protected override float GetOffset() protected override float GetOffset()
{ {
if (lineHeight <= 0f)
{
return 0f;
}
float offset = direction == Direction.Vertical ? viewportSize.y - lineHeight : viewportSize.x - lineHeight; float offset = direction == Direction.Vertical ? viewportSize.y - lineHeight : viewportSize.x - lineHeight;
return offset / 2; return offset / 2;
} }
public override int PositionToIndex(float position) public override int PositionToIndex(float position)
{ {
if (adapter == null || adapter.GetItemCount() <= 0)
{
return 0;
}
float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x; float len = direction == Direction.Vertical ? lineHeight + spacing.y : lineHeight + spacing.x;
if (len <= 0f)
{
return 0;
}
float pos = IndexToPosition(recyclerView.CurrentIndex); float pos = IndexToPosition(recyclerView.CurrentIndex);
// 根据是前划还是后划,来加减偏移量
int index = position > pos ? Mathf.RoundToInt(position / len + 0.25f) : Mathf.RoundToInt(position / len - 0.25f); int index = position > pos ? Mathf.RoundToInt(position / len + 0.25f) : Mathf.RoundToInt(position / len - 0.25f);
return index; return index;
@ -73,7 +94,7 @@ namespace AlicizaX.UI
public override void DoItemAnimation() public override void DoItemAnimation()
{ {
List<ViewHolder> viewHolders = viewProvider.ViewHolders; var viewHolders = viewProvider.ViewHolders;
for (int i = 0; i < viewHolders.Count; i++) for (int i = 0; i < viewHolders.Count; i++)
{ {
float viewPos = direction == Direction.Vertical ? -viewHolders[i].RectTransform.anchoredPosition.y : viewHolders[i].RectTransform.anchoredPosition.x; float viewPos = direction == Direction.Vertical ? -viewHolders[i].RectTransform.anchoredPosition.y : viewHolders[i].RectTransform.anchoredPosition.x;

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2450ac660a373c24caa2f5eba25c8237
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,49 @@
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace AlicizaX.UI
{
[DisallowMultipleComponent]
public sealed class RecyclerNavigationBridge : Selectable, ISelectHandler, IMoveHandler, ISubmitHandler
{
[SerializeField] private MoveDirection defaultEntryDirection = MoveDirection.Down;
private RecyclerView recyclerView;
protected override void Awake()
{
base.Awake();
transition = Transition.None;
Navigation navigationConfig = navigation;
navigationConfig.mode = Navigation.Mode.None;
navigation = navigationConfig;
recyclerView = GetComponent<RecyclerView>();
}
public override void OnSelect(BaseEventData eventData)
{
base.OnSelect(eventData);
TryEnter(defaultEntryDirection);
}
public override void OnMove(AxisEventData eventData)
{
if (!TryEnter(eventData.moveDir))
{
base.OnMove(eventData);
}
}
public void OnSubmit(BaseEventData eventData)
{
TryEnter(defaultEntryDirection);
}
private bool TryEnter(MoveDirection direction)
{
recyclerView ??= GetComponent<RecyclerView>();
return recyclerView != null && recyclerView.TryFocusEntry(direction);
}
}
}

View File

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

View File

@ -0,0 +1,97 @@
using UnityEngine;
using UnityEngine.EventSystems;
namespace AlicizaX.UI
{
public sealed class RecyclerNavigationController
{
private readonly RecyclerView recyclerView;
public RecyclerNavigationController(RecyclerView recyclerView)
{
this.recyclerView = recyclerView;
}
public bool TryMove(ViewHolder currentHolder, MoveDirection direction, RecyclerNavigationOptions options)
{
if (recyclerView == null ||
recyclerView.RecyclerViewAdapter == null ||
currentHolder == null)
{
return false;
}
int itemCount = recyclerView.RecyclerViewAdapter.GetItemCount();
int realCount = recyclerView.RecyclerViewAdapter.GetRealCount();
if (itemCount <= 0 || realCount <= 0)
{
return false;
}
int step = GetStep(direction);
if (step == 0)
{
return false;
}
bool isLoopSource = itemCount != realCount;
bool allowWrap = isLoopSource && options.Wrap;
int nextLayoutIndex = currentHolder.Index + step;
if (allowWrap)
{
nextLayoutIndex = WrapIndex(nextLayoutIndex, realCount);
}
else
{
nextLayoutIndex = Mathf.Clamp(nextLayoutIndex, 0, itemCount - 1);
}
if (!allowWrap && nextLayoutIndex == currentHolder.Index)
{
return false;
}
ScrollAlignment alignment = ResolveAlignment(direction, options.Alignment);
return recyclerView.TryFocusIndex(nextLayoutIndex, options.SmoothScroll, alignment);
}
private int GetStep(MoveDirection direction)
{
int unit = Mathf.Max(1, recyclerView.LayoutManager != null ? recyclerView.LayoutManager.Unit : 1);
bool vertical = recyclerView.Direction == Direction.Vertical;
return direction switch
{
MoveDirection.Left => vertical ? -1 : -unit,
MoveDirection.Right => vertical ? 1 : unit,
MoveDirection.Up => vertical ? -unit : -1,
MoveDirection.Down => vertical ? unit : 1,
_ => 0
};
}
private static int WrapIndex(int index, int count)
{
int wrapped = index % count;
return wrapped < 0 ? wrapped + count : wrapped;
}
private ScrollAlignment ResolveAlignment(MoveDirection direction, ScrollAlignment fallback)
{
if (recyclerView == null || recyclerView.LayoutManager == null)
{
return fallback;
}
bool vertical = recyclerView.Direction == Direction.Vertical;
return direction switch
{
MoveDirection.Down when vertical => ScrollAlignment.End,
MoveDirection.Up when vertical => ScrollAlignment.Start,
MoveDirection.Right when !vertical => ScrollAlignment.End,
MoveDirection.Left when !vertical => ScrollAlignment.Start,
_ => fallback
};
}
}
}

View File

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

View File

@ -0,0 +1,21 @@
namespace AlicizaX.UI
{
public readonly struct RecyclerNavigationOptions
{
public static readonly RecyclerNavigationOptions Clamped = new(false, false, ScrollAlignment.Center);
public static readonly RecyclerNavigationOptions Circular = new(true, false, ScrollAlignment.Center);
public RecyclerNavigationOptions(bool wrap, bool smoothScroll, ScrollAlignment alignment)
{
Wrap = wrap;
SmoothScroll = smoothScroll;
Alignment = alignment;
}
public bool Wrap { get; }
public bool SmoothScroll { get; }
public ScrollAlignment Alignment { get; }
}
}

View File

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

View File

@ -2,29 +2,12 @@ namespace AlicizaX.UI
{ {
public interface IObjectFactory<T> where T : class public interface IObjectFactory<T> where T : class
{ {
/// <summary>
/// 创建对象
/// </summary>
/// <returns></returns>
T Create(); T Create();
/// <summary>
/// 销毁对象
/// </summary>
/// <param name="obj"></param>
void Destroy(T obj); void Destroy(T obj);
/// <summary>
/// 重置对象
/// </summary>
/// <param name="obj"></param>
void Reset(T obj); void Reset(T obj);
/// <summary>
/// 验证对象
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
bool Validate(T obj); bool Validate(T obj);
} }
} }

View File

@ -4,16 +4,8 @@ namespace AlicizaX.UI
public interface IObjectPool : IDisposable public interface IObjectPool : IDisposable
{ {
/// <summary>
/// 从池子中分配一个可用对象,没有的话就创建一个
/// </summary>
/// <returns></returns>
object Allocate(); object Allocate();
/// <summary>
/// 将对象回收到池子中去,如果池中的对象数量已经超过了 maxSize则直接销毁该对象
/// </summary>
/// <param name="obj"></param>
void Free(object obj); void Free(object obj);
} }
@ -23,5 +15,4 @@ namespace AlicizaX.UI
void Free(T obj); void Free(T obj);
} }
} }

View File

@ -1,7 +0,0 @@
namespace AlicizaX.UI
{
public interface IPooledObject
{
void Free();
}
}

View File

@ -1,18 +1,22 @@
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
public class MixedObjectPool<T> : IMixedObjectPool<T> where T : class public class MixedObjectPool<T> : IMixedObjectPool<T> where T : class
{ {
private const int DEFAULT_MAX_SIZE_PER_TYPE = 10; private const int DEFAULT_MAX_SIZE_PER_TYPE = 10;
private readonly ConcurrentDictionary<string, List<T>> entries; private readonly Dictionary<string, Stack<T>> entries;
private readonly ConcurrentDictionary<string, int> typeSize; private readonly Dictionary<string, int> typeSize;
private readonly Dictionary<string, int> activeCountByType;
private readonly Dictionary<string, int> peakActiveByType;
private readonly IMixedObjectFactory<T> factory; private readonly IMixedObjectFactory<T> factory;
private int defaultMaxSizePerType; private readonly int defaultMaxSizePerType;
private int hitCount;
private int missCount;
private int destroyCount;
public MixedObjectPool(IMixedObjectFactory<T> factory) : this(factory, DEFAULT_MAX_SIZE_PER_TYPE) public MixedObjectPool(IMixedObjectFactory<T> factory) : this(factory, DEFAULT_MAX_SIZE_PER_TYPE)
{ {
@ -28,19 +32,25 @@ namespace AlicizaX.UI
throw new ArgumentException("The maxSize must be greater than 0."); throw new ArgumentException("The maxSize must be greater than 0.");
} }
entries = new ConcurrentDictionary<string, List<T>>(); entries = new Dictionary<string, Stack<T>>(StringComparer.Ordinal);
typeSize = new ConcurrentDictionary<string, int>(); typeSize = new Dictionary<string, int>(StringComparer.Ordinal);
activeCountByType = new Dictionary<string, int>(StringComparer.Ordinal);
peakActiveByType = new Dictionary<string, int>(StringComparer.Ordinal);
} }
public T Allocate(string typeName) public T Allocate(string typeName)
{ {
if (entries.TryGetValue(typeName, out List<T> list) && list.Count > 0) Stack<T> stack = GetOrCreateStack(typeName);
if (stack.Count > 0)
{ {
T obj = list[0]; T obj = stack.Pop();
list.RemoveAt(0); hitCount++;
TrackAllocate(typeName);
return obj; return obj;
} }
missCount++;
TrackAllocate(typeName);
return factory.Create(typeName); return factory.Create(typeName);
} }
@ -51,19 +61,25 @@ namespace AlicizaX.UI
if (!factory.Validate(typeName, obj)) if (!factory.Validate(typeName, obj))
{ {
factory.Destroy(typeName, obj); factory.Destroy(typeName, obj);
destroyCount++;
TrackFree(typeName);
return; return;
} }
int maxSize = GetMaxSize(typeName); int maxSize = GetMaxSize(typeName);
List<T> list = entries.GetOrAdd(typeName, n => new List<T>()); Stack<T> stack = GetOrCreateStack(typeName);
if (list.Count >= maxSize)
factory.Reset(typeName, obj);
TrackFree(typeName);
if (stack.Count >= maxSize)
{ {
factory.Destroy(typeName, obj); factory.Destroy(typeName, obj);
destroyCount++;
return; return;
} }
factory.Reset(typeName, obj); stack.Push(obj);
list.Add(obj);
} }
public int GetMaxSize(string typeName) public int GetMaxSize(string typeName)
@ -78,24 +94,79 @@ namespace AlicizaX.UI
public void SetMaxSize(string typeName, int value) public void SetMaxSize(string typeName, int value)
{ {
typeSize.AddOrUpdate(typeName, value, (key, oldValue) => value); typeSize[typeName] = value;
} }
public void EnsureCapacity(string typeName, int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
int current = GetMaxSize(typeName);
if (value > current)
{
typeSize[typeName] = value;
}
}
public void Warm(string typeName, int count)
{
if (count <= 0)
{
return;
}
int maxSize = GetMaxSize(typeName);
if (count > maxSize)
{
count = maxSize;
}
Stack<T> stack = GetOrCreateStack(typeName);
while (stack.Count < count)
{
stack.Push(factory.Create(typeName));
}
}
public int GetActiveCount(string typeName)
{
return activeCountByType.TryGetValue(typeName, out int count) ? count : 0;
}
public int GetPeakActiveCount(string typeName)
{
return peakActiveByType.TryGetValue(typeName, out int count) ? count : 0;
}
public int HitCount => hitCount;
public int MissCount => missCount;
public int DestroyCount => destroyCount;
protected virtual void Clear() protected virtual void Clear()
{ {
foreach (var kv in entries) foreach (var kv in entries)
{ {
string typeName = kv.Key; string typeName = kv.Key;
List<T> list = kv.Value; Stack<T> stack = kv.Value;
if (list == null || list.Count <= 0) continue; if (stack == null || stack.Count <= 0) continue;
list.ForEach(e => factory.Destroy(typeName, e)); while (stack.Count > 0)
list.Clear(); {
factory.Destroy(typeName, stack.Pop());
destroyCount++;
}
} }
entries.Clear(); entries.Clear();
typeSize.Clear(); typeSize.Clear();
activeCountByType.Clear();
peakActiveByType.Clear();
} }
public void Dispose() public void Dispose()
@ -103,6 +174,42 @@ namespace AlicizaX.UI
Clear(); Clear();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
private Stack<T> GetOrCreateStack(string typeName)
{
if (!entries.TryGetValue(typeName, out Stack<T> stack))
{
stack = new Stack<T>(GetMaxSize(typeName));
entries[typeName] = stack;
}
return stack;
}
private void TrackAllocate(string typeName)
{
int active = GetActiveCount(typeName) + 1;
activeCountByType[typeName] = active;
if (active > GetPeakActiveCount(typeName))
{
peakActiveByType[typeName] = active;
}
}
private void TrackFree(string typeName)
{
int active = GetActiveCount(typeName);
if (active > 0)
{
activeCountByType[typeName] = active - 1;
}
int recommendedMax = GetPeakActiveCount(typeName) + 1;
if (recommendedMax > GetMaxSize(typeName))
{
typeSize[typeName] = recommendedMax;
}
}
} }
} }

View File

@ -1,14 +1,20 @@
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
using System; using System;
using System.Threading; using System.Collections.Generic;
public class ObjectPool<T> : IObjectPool<T> where T : class public class ObjectPool<T> : IObjectPool<T> where T : class
{ {
private readonly Stack<T> entries;
private readonly int initialSize;
private int maxSize; private int maxSize;
private int initialSize;
protected readonly T[] entries = null;
protected readonly IObjectFactory<T> factory; protected readonly IObjectFactory<T> factory;
private int totalCount;
private int activeCount;
private int hitCount;
private int missCount;
private int destroyCount;
private int peakActive;
public ObjectPool(IObjectFactory<T> factory) : this(factory, Environment.ProcessorCount * 2) public ObjectPool(IObjectFactory<T> factory) : this(factory, Environment.ProcessorCount * 2)
{ {
@ -23,37 +29,62 @@ namespace AlicizaX.UI
this.factory = factory; this.factory = factory;
this.initialSize = initialSize; this.initialSize = initialSize;
this.maxSize = maxSize; this.maxSize = maxSize;
this.entries = new T[maxSize];
if (maxSize < initialSize) if (maxSize < initialSize)
{ {
throw new ArgumentException("The maxSize must be greater than or equal to the initialSize."); throw new ArgumentException("The maxSize must be greater than or equal to the initialSize.");
} }
for (int i = 0; i < initialSize; i++) entries = new Stack<T>(maxSize);
{ Warm(initialSize);
entries[i] = factory.Create();
}
} }
public int MaxSize => maxSize; public int MaxSize => maxSize;
public int InitialSize => initialSize; public int InitialSize => initialSize;
public int InactiveCount => entries.Count;
public int ActiveCount => activeCount;
public int TotalCount => totalCount;
public int PeakActive => peakActive;
public int HitCount => hitCount;
public int MissCount => missCount;
public int DestroyCount => destroyCount;
public virtual T Allocate() public virtual T Allocate()
{ {
for (var i = 0; i < entries.Length; i++) T value;
if (entries.Count > 0)
{ {
T value = entries[i]; value = entries.Pop();
if (value == null) continue; if (value != null)
if (Interlocked.CompareExchange(ref entries[i], null, value) == value)
{ {
hitCount++;
activeCount++;
if (activeCount > peakActive)
{
peakActive = activeCount;
}
return value; return value;
} }
} }
return factory.Create(); missCount++;
value = factory.Create();
totalCount++;
activeCount++;
if (activeCount > peakActive)
{
peakActive = activeCount;
}
return value;
} }
public virtual void Free(T obj) public virtual void Free(T obj)
@ -63,20 +94,39 @@ namespace AlicizaX.UI
if (!factory.Validate(obj)) if (!factory.Validate(obj))
{ {
factory.Destroy(obj); factory.Destroy(obj);
destroyCount++;
if (totalCount > 0)
{
totalCount--;
}
if (activeCount > 0)
{
activeCount--;
}
return; return;
} }
factory.Reset(obj); factory.Reset(obj);
for (var i = 0; i < entries.Length; i++) if (activeCount > 0)
{ {
if (Interlocked.CompareExchange(ref entries[i], obj, null) == null) activeCount--;
{
return;
} }
if (entries.Count < maxSize)
{
entries.Push(obj);
return;
} }
factory.Destroy(obj); factory.Destroy(obj);
destroyCount++;
if (totalCount > 0)
{
totalCount--;
}
} }
object IObjectPool.Allocate() object IObjectPool.Allocate()
@ -91,15 +141,17 @@ namespace AlicizaX.UI
protected virtual void Clear() protected virtual void Clear()
{ {
for (var i = 0; i < entries.Length; i++) while (entries.Count > 0)
{ {
var value = Interlocked.Exchange(ref entries[i], null); var value = entries.Pop();
if (value != null) if (value != null)
{ {
factory.Destroy(value); factory.Destroy(value);
destroyCount++;
} }
} }
totalCount = activeCount;
} }
public void Dispose() public void Dispose()
@ -107,6 +159,38 @@ namespace AlicizaX.UI
Clear(); Clear();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
public void EnsureCapacity(int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
if (value > maxSize)
{
maxSize = value;
}
}
public void Warm(int count)
{
if (count <= 0)
{
return;
}
if (count > maxSize)
{
count = maxSize;
}
while (totalCount < count)
{
entries.Push(factory.Create());
totalCount++;
}
}
} }
} }

View File

@ -27,8 +27,6 @@ namespace AlicizaX.UI
public void Reset(T obj) public void Reset(T obj)
{ {
obj.gameObject.SetActive(false); obj.gameObject.SetActive(false);
obj.gameObject.transform.position = Vector3.zero;
obj.gameObject.transform.rotation = Quaternion.identity;
} }
public bool Validate(T obj) public bool Validate(T obj)

View File

@ -21,8 +21,6 @@ namespace AlicizaX.UI
public virtual void Reset(GameObject obj) public virtual void Reset(GameObject obj)
{ {
obj.SetActive(false); obj.SetActive(false);
obj.transform.position = Vector3.zero;
obj.transform.rotation = Quaternion.identity;
} }
public virtual void Destroy(GameObject obj) public virtual void Destroy(GameObject obj)

View File

@ -1,6 +1,5 @@
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
@ -38,8 +37,18 @@ namespace AlicizaX.UI
public T Create(string typeName) public T Create(string typeName)
{ {
T obj = Object.Instantiate(dict[typeName], parent); T obj = Object.Instantiate(dict[typeName], parent);
obj.transform.position = Vector3.zero; if (obj.transform is RectTransform rectTransform)
obj.transform.rotation = Quaternion.identity; {
rectTransform.anchoredPosition3D = Vector3.zero;
rectTransform.localRotation = Quaternion.identity;
rectTransform.localScale = Vector3.one;
}
else
{
obj.transform.localPosition = Vector3.zero;
obj.transform.localRotation = Quaternion.identity;
obj.transform.localScale = Vector3.one;
}
return obj; return obj;
} }
@ -52,8 +61,6 @@ namespace AlicizaX.UI
public void Reset(string typeName, T obj) public void Reset(string typeName, T obj)
{ {
obj.gameObject.SetActive(false); obj.gameObject.SetActive(false);
obj.transform.position = Vector3.zero;
obj.transform.rotation = Quaternion.identity;
} }
public bool Validate(string typeName, T obj) public bool Validate(string typeName, T obj)

View File

@ -31,8 +31,6 @@ namespace AlicizaX.UI
public void Reset(string typeName, GameObject obj) public void Reset(string typeName, GameObject obj)
{ {
obj.SetActive(false); obj.SetActive(false);
obj.transform.position = Vector3.zero;
obj.transform.rotation = Quaternion.identity;
} }
public bool Validate(string typeName, GameObject obj) public bool Validate(string typeName, GameObject obj)

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,9 @@
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
/// <summary>
/// 定义滚动到列表项时的对齐方式
/// </summary>
public enum ScrollAlignment public enum ScrollAlignment
{ {
/// <summary>
/// 将列表项对齐到视口的顶部/左侧
/// </summary>
Start, Start,
/// <summary>
/// 将列表项对齐到视口的中心
/// </summary>
Center, Center,
/// <summary>
/// 将列表项对齐到视口的底部/右侧
/// </summary>
End End
} }
} }

View File

@ -2,37 +2,16 @@ using UnityEngine.Events;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
/// <summary>
/// 滚动控制器接口
/// 定义滚动行为的基本契约
/// </summary>
public interface IScroller public interface IScroller
{ {
/// <summary>
/// 获取或设置当前滚动位置
/// </summary>
float Position { get; set; } float Position { get; set; }
/// <summary>
/// 滚动到指定位置
/// </summary>
/// <param name="position">目标位置</param>
/// <param name="smooth">是否使用平滑滚动</param>
void ScrollTo(float position, bool smooth = false); void ScrollTo(float position, bool smooth = false);
} }
/// <summary>
/// 滚动位置改变事件
/// </summary>
public class ScrollerEvent : UnityEvent<float> { } public class ScrollerEvent : UnityEvent<float> { }
/// <summary>
/// 滚动停止事件
/// </summary>
public class MoveStopEvent : UnityEvent { } public class MoveStopEvent : UnityEvent { }
/// <summary>
/// 拖拽状态改变事件
/// </summary>
public class DraggingEvent : UnityEvent<bool> { } public class DraggingEvent : UnityEvent<bool> { }
} }

View File

@ -1,4 +1,4 @@
using System.Collections; using System.Collections;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
@ -6,6 +6,9 @@ namespace AlicizaX.UI
{ {
public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler
{ {
private static readonly WaitForEndOfFrame EndOfFrameYield = new();
private Coroutine movementCoroutine;
protected float position; protected float position;
public float Position { get => position; set => position = value; } public float Position { get => position; set => position = value; }
@ -19,9 +22,6 @@ namespace AlicizaX.UI
set => direction = value; set => direction = value;
} }
/// <summary>
/// 内容所需要大小
/// </summary>
protected Vector2 contentSize; protected Vector2 contentSize;
public Vector2 ContentSize public Vector2 ContentSize
{ {
@ -29,9 +29,6 @@ namespace AlicizaX.UI
set => contentSize = value; set => contentSize = value;
} }
/// <summary>
/// 所在 View 的真实大小
/// </summary>
protected Vector2 viewSize; protected Vector2 viewSize;
public Vector2 ViewSize public Vector2 ViewSize
{ {
@ -76,12 +73,14 @@ namespace AlicizaX.UI
public DraggingEvent OnDragging { get => draggingEvent; set => draggingEvent = value; } public DraggingEvent OnDragging { get => draggingEvent; set => draggingEvent = value; }
// 停止滑动的时间,但此时并未释放鼠标按键
public float dragStopTime = 0f; public float dragStopTime = 0f;
public virtual void ScrollTo(float position, bool smooth = false) public virtual void ScrollTo(float position, bool smooth = false)
{ {
if (position == this.position) return; if (Mathf.Approximately(position, this.position)) return;
StopMovement();
if (!smooth) if (!smooth)
{ {
@ -90,11 +89,28 @@ namespace AlicizaX.UI
} }
else else
{ {
StopAllCoroutines(); movementCoroutine = StartCoroutine(RunMotion(MoveTo(position)));
StartCoroutine(MoveTo(position));
} }
} }
public virtual void ScrollToDuration(float position, float duration)
{
if (Mathf.Approximately(position, this.position))
{
return;
}
StopMovement();
if (duration <= 0f)
{
this.position = position;
OnValueChanged?.Invoke(this.position);
return;
}
movementCoroutine = StartCoroutine(RunMotion(ToPositionByDuration(position, duration)));
}
public virtual void ScrollToRatio(float ratio) public virtual void ScrollToRatio(float ratio)
{ {
ScrollTo(MaxPosition * ratio, false); ScrollTo(MaxPosition * ratio, false);
@ -103,7 +119,7 @@ namespace AlicizaX.UI
public void OnBeginDrag(PointerEventData eventData) public void OnBeginDrag(PointerEventData eventData)
{ {
OnDragging?.Invoke(true); OnDragging?.Invoke(true);
StopAllCoroutines(); StopMovement();
} }
public void OnEndDrag(PointerEventData eventData) public void OnEndDrag(PointerEventData eventData)
@ -125,7 +141,7 @@ namespace AlicizaX.UI
public void OnScroll(PointerEventData eventData) public void OnScroll(PointerEventData eventData)
{ {
StopAllCoroutines(); StopMovement();
float rate = GetScrollRate() * wheelSpeed; float rate = GetScrollRate() * wheelSpeed;
velocity = direction == Direction.Vertical ? -eventData.scrollDelta.y * rate : eventData.scrollDelta.x * rate; velocity = direction == Direction.Vertical ? -eventData.scrollDelta.y * rate : eventData.scrollDelta.x * rate;
@ -156,18 +172,13 @@ namespace AlicizaX.UI
return rate; return rate;
} }
/// <summary>
/// 松手时的惯性滑动
/// </summary>
protected virtual void Inertia() protected virtual void Inertia()
{ {
// 松手时的时间 离 停止滑动的时间 超过一定时间,则认为此次惯性滑动无效
if (!snap && (Time.time - dragStopTime) > 0.01f) return;
if (Mathf.Abs(velocity) > 0.1f) if (Mathf.Abs(velocity) > 0.1f)
{ {
StopAllCoroutines(); StopMovement();
StartCoroutine(InertiaTo()); movementCoroutine = StartCoroutine(RunMotion(InertiaTo()));
} }
else else
{ {
@ -175,20 +186,17 @@ namespace AlicizaX.UI
} }
} }
/// <summary>
/// 滑动到顶部/底部之后,松手时回弹
/// </summary>
protected virtual void Elastic() protected virtual void Elastic()
{ {
if (position < 0) if (position < 0)
{ {
StopAllCoroutines(); StopMovement();
StartCoroutine(ElasticTo(0)); movementCoroutine = StartCoroutine(RunMotion(ElasticTo(0)));
} }
else if (position > MaxPosition) else if (position > MaxPosition)
{ {
StopAllCoroutines(); StopMovement();
StartCoroutine(ElasticTo(MaxPosition)); movementCoroutine = StartCoroutine(RunMotion(ElasticTo(MaxPosition)));
} }
} }
@ -208,7 +216,7 @@ namespace AlicizaX.UI
OnValueChanged?.Invoke(position); OnValueChanged?.Invoke(position);
yield return new WaitForEndOfFrame(); yield return EndOfFrameYield;
} }
OnMoveStoped?.Invoke(); OnMoveStoped?.Invoke();
@ -224,6 +232,24 @@ namespace AlicizaX.UI
yield return ToPosition(targetPos, scrollSpeed); yield return ToPosition(targetPos, scrollSpeed);
} }
IEnumerator ToPositionByDuration(float targetPos, float duration)
{
duration = Mathf.Max(duration, 0.0001f);
float startPos = position;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
position = Mathf.Lerp(startPos, targetPos, t);
OnValueChanged?.Invoke(position);
yield return EndOfFrameYield;
}
position = targetPos;
OnValueChanged?.Invoke(position);
}
IEnumerator ToPosition(float targetPos, float speed) IEnumerator ToPosition(float targetPos, float speed)
{ {
float startPos = position; float startPos = position;
@ -235,11 +261,28 @@ namespace AlicizaX.UI
time += Time.deltaTime; time += Time.deltaTime;
yield return new WaitForEndOfFrame(); yield return EndOfFrameYield;
} }
position = targetPos; position = targetPos;
OnValueChanged?.Invoke(position); OnValueChanged?.Invoke(position);
} }
private IEnumerator RunMotion(IEnumerator motion)
{
yield return motion;
movementCoroutine = null;
}
private void StopMovement()
{
if (movementCoroutine == null)
{
return;
}
StopCoroutine(movementCoroutine);
movementCoroutine = null;
}
} }
} }

View File

@ -3,13 +3,33 @@ using System.Collections.Generic;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
/// <summary>
/// 封装 RecyclerView 与 Adapter 的通用列表基类。
/// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
/// <typeparam name="TAdapter">适配器类型。</typeparam>
public abstract class UGListBase<TData, TAdapter> where TAdapter : Adapter<TData> where TData : ISimpleViewData public abstract class UGListBase<TData, TAdapter> where TAdapter : Adapter<TData> where TData : ISimpleViewData
{ {
/// <summary>
/// 关联的 RecyclerView 实例。
/// </summary>
protected readonly RecyclerView _recyclerView; protected readonly RecyclerView _recyclerView;
/// <summary>
/// 当前列表使用的适配器实例。
/// </summary>
protected readonly TAdapter _adapter; protected readonly TAdapter _adapter;
/// <summary>
/// 获取当前绑定的 RecyclerView。
/// </summary>
public RecyclerView RecyclerView => _recyclerView; public RecyclerView RecyclerView => _recyclerView;
/// <summary>
/// 初始化列表封装并将适配器绑定到 RecyclerView。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
/// <param name="adapter">用于驱动列表渲染的适配器。</param>
public UGListBase(RecyclerView recyclerView, TAdapter adapter) public UGListBase(RecyclerView recyclerView, TAdapter adapter)
{ {
_recyclerView = recyclerView; _recyclerView = recyclerView;
@ -21,30 +41,60 @@ namespace AlicizaX.UI
} }
} }
/// <summary>
/// 获取当前列表使用的适配器。
/// </summary>
public TAdapter Adapter => _adapter; public TAdapter Adapter => _adapter;
public void RegisterItemRender<TItemRender>(string viewName = "") where TItemRender : IItemRender /// <summary>
/// 注册指定视图类型对应的 ItemRender。
/// </summary>
/// <typeparam name="TItemRender">ItemRender 类型。</typeparam>
/// <param name="viewName">视图名称;为空时表示默认视图。</param>
public void RegisterItemRender<TItemRender>(string viewName = "") where TItemRender : ItemRenderBase
{ {
_adapter.RegisterItemRender<TItemRender>(viewName); _adapter.RegisterItemRender<TItemRender>(viewName);
} }
/// <summary>
/// 按运行时类型注册指定视图对应的 ItemRender。
/// </summary>
/// <param name="itemRenderType">ItemRender 的运行时类型。</param>
/// <param name="viewName">视图名称;为空时表示默认视图。</param>
public void RegisterItemRender(Type itemRenderType, string viewName = "") public void RegisterItemRender(Type itemRenderType, string viewName = "")
{ {
_adapter.RegisterItemRender(itemRenderType, viewName); _adapter.RegisterItemRender(itemRenderType, viewName);
} }
/// <summary>
/// 注销指定视图名称对应的 ItemRender 注册。
/// </summary>
/// <param name="viewName">视图名称;为空时表示默认视图。</param>
/// <returns>是否成功移除对应注册。</returns>
public bool UnregisterItemRender(string viewName = "") public bool UnregisterItemRender(string viewName = "")
{ {
return _adapter.UnregisterItemRender(viewName); return _adapter.UnregisterItemRender(viewName);
} }
/// <summary>
/// 清空当前列表的全部 ItemRender 注册信息。
/// </summary>
public void ClearItemRenderRegistrations() public void ClearItemRenderRegistrations()
{ {
_adapter.ClearItemRenderRegistrations(); _adapter.ClearItemRenderRegistrations();
} }
/// <summary>
/// 当前持有的数据集合引用。
/// </summary>
private List<TData> _datas; private List<TData> _datas;
/// <summary>
/// 获取或设置当前列表数据。
/// </summary>
/// <remarks>
/// 设置数据时会同步调用适配器刷新列表内容。
/// </remarks>
public List<TData> Data public List<TData> Data
{ {
get => _datas; get => _datas;
@ -56,49 +106,110 @@ namespace AlicizaX.UI
} }
} }
/// <summary>
/// 提供单模板列表的便捷封装。
/// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
public class UGList<TData> : UGListBase<TData, Adapter<TData>> where TData : ISimpleViewData public class UGList<TData> : UGListBase<TData, Adapter<TData>> where TData : ISimpleViewData
{ {
/// <summary>
/// 初始化单模板列表。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
public UGList(RecyclerView recyclerView) public UGList(RecyclerView recyclerView)
: base(recyclerView, new Adapter<TData>(recyclerView)) : base(recyclerView, new Adapter<TData>(recyclerView))
{ {
} }
} }
/// <summary>
/// 提供分组列表的便捷封装。
/// </summary>
/// <typeparam name="TData">分组列表数据类型。</typeparam>
public class UGGroupList<TData> : UGListBase<TData, GroupAdapter<TData>> where TData : class, IGroupViewData, new() public class UGGroupList<TData> : UGListBase<TData, GroupAdapter<TData>> where TData : class, IGroupViewData, new()
{ {
/// <summary>
/// 初始化分组列表。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
/// <param name="groupViewName">分组头使用的模板名称。</param>
public UGGroupList(RecyclerView recyclerView, string groupViewName) public UGGroupList(RecyclerView recyclerView, string groupViewName)
: base(recyclerView, new GroupAdapter<TData>(recyclerView, groupViewName)) : base(recyclerView, new GroupAdapter<TData>(recyclerView, groupViewName))
{ {
} }
} }
/// <summary>
/// 提供循环列表的便捷封装。
/// </summary>
/// <typeparam name="TData">循环列表数据类型。</typeparam>
public class UGLoopList<TData> : UGListBase<TData, LoopAdapter<TData>> where TData : ISimpleViewData, new() public class UGLoopList<TData> : UGListBase<TData, LoopAdapter<TData>> where TData : ISimpleViewData, new()
{ {
/// <summary>
/// 初始化循环列表。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
public UGLoopList(RecyclerView recyclerView) public UGLoopList(RecyclerView recyclerView)
: base(recyclerView, new LoopAdapter<TData>(recyclerView)) : base(recyclerView, new LoopAdapter<TData>(recyclerView))
{ {
} }
} }
/// <summary>
/// 提供多模板列表的便捷封装。
/// </summary>
/// <typeparam name="TData">多模板列表数据类型。</typeparam>
public class UGMixedList<TData> : UGListBase<TData, MixedAdapter<TData>> where TData : IMixedViewData public class UGMixedList<TData> : UGListBase<TData, MixedAdapter<TData>> where TData : IMixedViewData
{ {
/// <summary>
/// 初始化多模板列表。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
public UGMixedList(RecyclerView recyclerView) public UGMixedList(RecyclerView recyclerView)
: base(recyclerView, new MixedAdapter<TData>(recyclerView)) : base(recyclerView, new MixedAdapter<TData>(recyclerView))
{ {
} }
} }
/// <summary>
/// 提供常用 UGList 类型的快速创建方法。
/// </summary>
public static class UGListCreateHelper public static class UGListCreateHelper
{ {
/// <summary>
/// 创建单模板列表封装。
/// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
/// <param name="recyclerView">目标 RecyclerView。</param>
/// <returns>创建后的单模板列表实例。</returns>
public static UGList<TData> Create<TData>(RecyclerView recyclerView) where TData : ISimpleViewData public static UGList<TData> Create<TData>(RecyclerView recyclerView) where TData : ISimpleViewData
=> new UGList<TData>(recyclerView); => new UGList<TData>(recyclerView);
/// <summary>
/// 创建分组列表封装。
/// </summary>
/// <typeparam name="TData">分组列表数据类型。</typeparam>
/// <param name="recyclerView">目标 RecyclerView。</param>
/// <param name="groupViewName">分组头使用的模板名称。</param>
/// <returns>创建后的分组列表实例。</returns>
public static UGGroupList<TData> CreateGroup<TData>(RecyclerView recyclerView, string groupViewName) where TData : class, IGroupViewData, new() public static UGGroupList<TData> CreateGroup<TData>(RecyclerView recyclerView, string groupViewName) where TData : class, IGroupViewData, new()
=> new UGGroupList<TData>(recyclerView, groupViewName); => new UGGroupList<TData>(recyclerView, groupViewName);
/// <summary>
/// 创建循环列表封装。
/// </summary>
/// <typeparam name="TData">循环列表数据类型。</typeparam>
/// <param name="recyclerView">目标 RecyclerView。</param>
/// <returns>创建后的循环列表实例。</returns>
public static UGLoopList<TData> CreateLoop<TData>(RecyclerView recyclerView) where TData : ISimpleViewData, new() public static UGLoopList<TData> CreateLoop<TData>(RecyclerView recyclerView) where TData : ISimpleViewData, new()
=> new UGLoopList<TData>(recyclerView); => new UGLoopList<TData>(recyclerView);
/// <summary>
/// 创建多模板列表封装。
/// </summary>
/// <typeparam name="TData">多模板列表数据类型。</typeparam>
/// <param name="recyclerView">目标 RecyclerView。</param>
/// <returns>创建后的多模板列表实例。</returns>
public static UGMixedList<TData> CreateMixed<TData>(RecyclerView recyclerView) where TData : IMixedViewData public static UGMixedList<TData> CreateMixed<TData>(RecyclerView recyclerView) where TData : IMixedViewData
=> new UGMixedList<TData>(recyclerView); => new UGMixedList<TData>(recyclerView);
} }

View File

@ -1,28 +1,29 @@
using System; using System;
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
/// <summary> /// <summary>
/// UGList 扩展方法类 /// 提供 UGList 的常用扩展方法。
/// 提供增强的滚动功能
/// </summary> /// </summary>
public static class UGListExtensions public static class UGListExtensions
{ {
/// <summary> /// <summary>
/// 启用 ScrollTo 操作的调试日志 /// 控制是否输出滚动定位调试日志。
/// </summary> /// </summary>
public static bool DebugScrollTo { get; set; } = false; public static bool DebugScrollTo { get; set; } = false;
/// <summary> /// <summary>
/// 滚动到指定的列表项,支持对齐方式和动画选项 /// 将列表滚动到指定索引,并按给定对齐方式定位。
/// </summary> /// </summary>
/// <param name="ugList">UGList 实例</param> /// <typeparam name="TData">列表数据类型。</typeparam>
/// <param name="index">要滚动到的列表项索引</param> /// <typeparam name="TAdapter">适配器类型。</typeparam>
/// <param name="alignment">列表项在视口中的对齐方式(起始、居中或结束)</param> /// <param name="ugList">目标列表实例。</param>
/// <param name="offset">对齐后额外应用的偏移量(像素)</param> /// <param name="index">目标数据索引。</param>
/// <param name="smooth">是否使用动画滚动</param> /// <param name="alignment">滚动完成后的对齐方式。</param>
/// <param name="duration">动画持续时间(秒),仅在 smooth 为 true 时使用</param> /// <param name="offset">在对齐基础上的额外偏移量。</param>
/// <param name="smooth">是否使用平滑滚动。</param>
/// <param name="duration">平滑滚动时长,单位为秒。</param>
public static void ScrollTo<TData, TAdapter>( public static void ScrollTo<TData, TAdapter>(
this UGListBase<TData, TAdapter> ugList, this UGListBase<TData, TAdapter> ugList,
int index, int index,
@ -48,8 +49,15 @@ namespace AlicizaX.UI
} }
/// <summary> /// <summary>
/// 滚动到指定的列表项并将其对齐到视口的起始位置(顶部/左侧) /// 将列表滚动到指定索引,并使目标项靠近起始端。
/// </summary> /// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
/// <typeparam name="TAdapter">适配器类型。</typeparam>
/// <param name="ugList">目标列表实例。</param>
/// <param name="index">目标数据索引。</param>
/// <param name="offset">在起始对齐基础上的额外偏移量。</param>
/// <param name="smooth">是否使用平滑滚动。</param>
/// <param name="duration">平滑滚动时长,单位为秒。</param>
public static void ScrollToStart<TData, TAdapter>( public static void ScrollToStart<TData, TAdapter>(
this UGListBase<TData, TAdapter> ugList, this UGListBase<TData, TAdapter> ugList,
int index, int index,
@ -63,8 +71,15 @@ namespace AlicizaX.UI
} }
/// <summary> /// <summary>
/// 滚动到指定的列表项并将其对齐到视口的中心位置 /// 将列表滚动到指定索引,并使目标项居中显示。
/// </summary> /// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
/// <typeparam name="TAdapter">适配器类型。</typeparam>
/// <param name="ugList">目标列表实例。</param>
/// <param name="index">目标数据索引。</param>
/// <param name="offset">在居中对齐基础上的额外偏移量。</param>
/// <param name="smooth">是否使用平滑滚动。</param>
/// <param name="duration">平滑滚动时长,单位为秒。</param>
public static void ScrollToCenter<TData, TAdapter>( public static void ScrollToCenter<TData, TAdapter>(
this UGListBase<TData, TAdapter> ugList, this UGListBase<TData, TAdapter> ugList,
int index, int index,
@ -78,8 +93,15 @@ namespace AlicizaX.UI
} }
/// <summary> /// <summary>
/// 滚动到指定的列表项并将其对齐到视口的结束位置(底部/右侧) /// 将列表滚动到指定索引,并使目标项靠近末端。
/// </summary> /// </summary>
/// <typeparam name="TData">列表数据类型。</typeparam>
/// <typeparam name="TAdapter">适配器类型。</typeparam>
/// <param name="ugList">目标列表实例。</param>
/// <param name="index">目标数据索引。</param>
/// <param name="offset">在末端对齐基础上的额外偏移量。</param>
/// <param name="smooth">是否使用平滑滚动。</param>
/// <param name="duration">平滑滚动时长,单位为秒。</param>
public static void ScrollToEnd<TData, TAdapter>( public static void ScrollToEnd<TData, TAdapter>(
this UGListBase<TData, TAdapter> ugList, this UGListBase<TData, TAdapter> ugList,
int index, int index,

View File

@ -1,22 +0,0 @@
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

@ -6,9 +6,6 @@ namespace AlicizaX.UI
public abstract class ViewHolder : MonoBehaviour public abstract class ViewHolder : MonoBehaviour
{ {
private RectTransform rectTransform; private RectTransform rectTransform;
private Action clickAction;
private Action pointerEnterAction;
private Action pointerExitAction;
internal event Action<ViewHolder> Destroyed; internal event Action<ViewHolder> Destroyed;
@ -23,53 +20,33 @@ namespace AlicizaX.UI
return rectTransform; return rectTransform;
} }
private set => rectTransform = value;
} }
public string Name { get; internal set; } public string Name { get; internal set; }
public int Index { get; internal set; } public int Index { get; internal set; }
public int DataIndex { get; internal set; } = -1;
public RecyclerView RecyclerView { get; internal set; }
public uint BindingVersion { get; private set; }
public Vector2 SizeDelta => RectTransform.sizeDelta; public Vector2 SizeDelta => RectTransform.sizeDelta;
internal uint AdvanceBindingVersion()
{
BindingVersion = BindingVersion == uint.MaxValue ? 1u : BindingVersion + 1u;
return BindingVersion;
}
protected internal virtual void OnRecycled() protected internal virtual void OnRecycled()
{ {
} AdvanceBindingVersion();
Name = string.Empty;
internal void SetInteractionCallbacks( Index = -1;
Action clickAction = null, DataIndex = -1;
Action pointerEnterAction = null, RecyclerView = null;
Action pointerExitAction = null)
{
this.clickAction = clickAction;
this.pointerEnterAction = pointerEnterAction;
this.pointerExitAction = pointerExitAction;
}
public void ClearInteractionCallbacks()
{
clickAction = null;
pointerEnterAction = null;
pointerExitAction = null;
}
protected virtual void OnClearInteractionCallbacks()
{
}
protected void InvokeClickAction()
{
clickAction?.Invoke();
}
protected void InvokePointerEnterAction()
{
pointerEnterAction?.Invoke();
}
protected void InvokePointerExitAction()
{
pointerExitAction?.Invoke();
} }
protected virtual void OnDestroy() protected virtual void OnDestroy()

View File

@ -1,26 +1,30 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
public class MixedViewProvider : ViewProvider public class MixedViewProvider : ViewProvider
{ {
[SerializeField] private ViewHolder chatLeftViewHolder; private readonly MixedObjectPool<ViewHolder> objectPool;
[SerializeField] private ViewHolder chatRightViewHolder; private readonly Dictionary<string, ViewHolder> templatesByName = new(StringComparer.Ordinal);
private IMixedObjectPool<ViewHolder> objectPool; public override string PoolStats =>
private Dictionary<string, ViewHolder> dict = new(); $"hits={objectPool.HitCount}, misses={objectPool.MissCount}, destroys={objectPool.DestroyCount}";
public MixedViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates) public MixedViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates)
{ {
foreach (var template in templates) for (int i = 0; i < templates.Length; i++)
{ {
dict[template.name] = template; ViewHolder template = templates[i];
if (template == null)
{
continue;
} }
UnityMixedComponentFactory<ViewHolder> factory = new(dict, recyclerView.Content); templatesByName[template.name] = template;
}
UnityMixedComponentFactory<ViewHolder> factory = new(templatesByName, recyclerView.Content);
objectPool = new MixedObjectPool<ViewHolder>(factory); objectPool = new MixedObjectPool<ViewHolder>(factory);
} }
@ -30,7 +34,13 @@ namespace AlicizaX.UI
{ {
throw new NullReferenceException("ViewProvider templates can not null or empty."); throw new NullReferenceException("ViewProvider templates can not null or empty.");
} }
return dict[viewName];
if (!templatesByName.TryGetValue(viewName, out ViewHolder template))
{
throw new KeyNotFoundException($"ViewProvider template '{viewName}' was not found.");
}
return template;
} }
public override ViewHolder[] GetTemplates() public override ViewHolder[] GetTemplates()
@ -39,7 +49,10 @@ namespace AlicizaX.UI
{ {
throw new NullReferenceException("ViewProvider templates can not null or empty."); throw new NullReferenceException("ViewProvider templates can not null or empty.");
} }
return dict.Values.ToArray();
ViewHolder[] values = new ViewHolder[templatesByName.Count];
templatesByName.Values.CopyTo(values, 0);
return values;
} }
public override ViewHolder Allocate(string viewName) public override ViewHolder Allocate(string viewName)
@ -60,5 +73,38 @@ namespace AlicizaX.UI
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders(); (Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
objectPool.Dispose(); objectPool.Dispose();
} }
public override void PreparePool()
{
int warmCount = GetRecommendedWarmCount();
if (warmCount <= 0)
{
return;
}
int itemCount = GetItemCount();
int start = Math.Max(0, LayoutManager.GetStartIndex());
int end = Math.Min(itemCount - 1, start + warmCount - 1);
Dictionary<string, int> counts = new(StringComparer.Ordinal);
for (int index = start; index <= end; index++)
{
string viewName = Adapter.GetViewName(index);
if (string.IsNullOrEmpty(viewName))
{
continue;
}
counts.TryGetValue(viewName, out int count);
counts[viewName] = count + 1;
}
foreach (var pair in counts)
{
int targetCount = pair.Value + Math.Max(1, LayoutManager.Unit);
objectPool.EnsureCapacity(pair.Key, targetCount);
objectPool.Warm(pair.Key, targetCount);
}
}
} }
} }

View File

@ -4,12 +4,15 @@ namespace AlicizaX.UI
{ {
public sealed class SimpleViewProvider : ViewProvider public sealed class SimpleViewProvider : ViewProvider
{ {
private readonly IObjectPool<ViewHolder> objectPool; private readonly ObjectPool<ViewHolder> objectPool;
public override string PoolStats =>
$"hits={objectPool.HitCount}, misses={objectPool.MissCount}, destroys={objectPool.DestroyCount}, active={objectPool.ActiveCount}, inactive={objectPool.InactiveCount}, peakActive={objectPool.PeakActive}, capacity={objectPool.MaxSize}";
public SimpleViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates) public SimpleViewProvider(RecyclerView recyclerView, ViewHolder[] templates) : base(recyclerView, templates)
{ {
UnityComponentFactory<ViewHolder> factory = new(GetTemplate(), recyclerView.Content); UnityComponentFactory<ViewHolder> factory = new(GetTemplate(), recyclerView.Content);
objectPool = new ObjectPool<ViewHolder>(factory, 100); objectPool = new ObjectPool<ViewHolder>(factory, 0, 1);
} }
public override ViewHolder GetTemplate(string viewName = "") public override ViewHolder GetTemplate(string viewName = "")
@ -48,5 +51,17 @@ namespace AlicizaX.UI
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders(); (Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
objectPool.Dispose(); objectPool.Dispose();
} }
public override void PreparePool()
{
int warmCount = GetRecommendedWarmCount();
if (warmCount <= 0)
{
return;
}
objectPool.EnsureCapacity(warmCount);
objectPool.Warm(warmCount);
}
} }
} }

View File

@ -1,82 +1,45 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems;
namespace AlicizaX.UI namespace AlicizaX.UI
{ {
/// <summary>
/// 提供和管理 ViewHolder
/// 负责 ViewHolder 的创建、回收和复用
/// </summary>
public abstract class ViewProvider public abstract class ViewProvider
{ {
private readonly List<ViewHolder> viewHolders = new(); private readonly List<ViewHolder> viewHolders = new();
private readonly Dictionary<int, ViewHolder> viewHoldersByIndex = new();
private readonly Dictionary<int, List<ViewHolder>> viewHoldersByDataIndex = new();
private readonly Dictionary<int, int> viewHolderPositions = new();
/// <summary>
/// 获取或设置数据适配器
/// </summary>
public IAdapter Adapter { get; set; } public IAdapter Adapter { get; set; }
/// <summary>
/// 获取或设置布局管理器
/// </summary>
public LayoutManager LayoutManager { get; set; } public LayoutManager LayoutManager { get; set; }
/// <summary> public IReadOnlyList<ViewHolder> ViewHolders => viewHolders;
/// 获取当前所有活动的 ViewHolder 列表
/// </summary> public abstract string PoolStats { get; }
public List<ViewHolder> ViewHolders => viewHolders;
protected RecyclerView recyclerView; protected RecyclerView recyclerView;
protected ViewHolder[] templates; protected ViewHolder[] templates;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">关联的 RecyclerView 实例</param>
/// <param name="templates">ViewHolder 模板数组</param>
public ViewProvider(RecyclerView recyclerView, ViewHolder[] templates) public ViewProvider(RecyclerView recyclerView, ViewHolder[] templates)
{ {
this.recyclerView = recyclerView; this.recyclerView = recyclerView;
this.templates = templates; this.templates = templates;
} }
/// <summary>
/// 根据视图名称获取对应的模板(抽象方法,由子类实现)
/// </summary>
/// <param name="viewName">视图名称</param>
/// <returns>对应的 ViewHolder 模板</returns>
public abstract ViewHolder GetTemplate(string viewName); public abstract ViewHolder GetTemplate(string viewName);
/// <summary>
/// 获取所有模板(抽象方法,由子类实现)
/// </summary>
/// <returns>所有 ViewHolder 模板数组</returns>
public abstract ViewHolder[] GetTemplates(); public abstract ViewHolder[] GetTemplates();
/// <summary>
/// 从对象池中分配一个 ViewHolder抽象方法由子类实现
/// </summary>
/// <param name="viewName">视图名称</param>
/// <returns>分配的 ViewHolder 实例</returns>
public abstract ViewHolder Allocate(string viewName); public abstract ViewHolder Allocate(string viewName);
/// <summary>
/// 将 ViewHolder 回收到对象池(抽象方法,由子类实现)
/// </summary>
/// <param name="viewName">视图名称</param>
/// <param name="viewHolder">要回收的 ViewHolder</param>
public abstract void Free(string viewName, ViewHolder viewHolder); public abstract void Free(string viewName, ViewHolder viewHolder);
/// <summary>
/// 重置 ViewProvider 状态(抽象方法,由子类实现)
/// </summary>
public abstract void Reset(); public abstract void Reset();
/// <summary> public abstract void PreparePool();
/// 创建指定索引的 ViewHolder
/// 从对象池中获取或创建新的 ViewHolder并进行布局和数据绑定
/// </summary>
/// <param name="index">数据索引</param>
public void CreateViewHolder(int index) public void CreateViewHolder(int index)
{ {
for (int i = index; i < index + LayoutManager.Unit; i++) for (int i = index; i < index + LayoutManager.Unit; i++)
@ -87,18 +50,16 @@ namespace AlicizaX.UI
var viewHolder = Allocate(viewName); var viewHolder = Allocate(viewName);
viewHolder.Name = viewName; viewHolder.Name = viewName;
viewHolder.Index = i; viewHolder.Index = i;
viewHolder.DataIndex = i;
viewHolder.RecyclerView = recyclerView;
viewHolders.Add(viewHolder); viewHolders.Add(viewHolder);
RegisterViewHolder(viewHolder);
LayoutManager.Layout(viewHolder, i); LayoutManager.Layout(viewHolder, i);
Adapter.OnBindViewHolder(viewHolder, i); Adapter.OnBindViewHolder(viewHolder, i);
} }
} }
/// <summary>
/// 移除指定索引的 ViewHolder
/// 将 ViewHolder 从活动列表中移除并回收到对象池
/// </summary>
/// <param name="index">数据索引</param>
public void RemoveViewHolder(int index) public void RemoveViewHolder(int index)
{ {
for (int i = index; i < index + LayoutManager.Unit; i++) for (int i = index; i < index + LayoutManager.Unit; i++)
@ -110,83 +71,171 @@ namespace AlicizaX.UI
if (viewHolderIndex < 0 || viewHolderIndex >= viewHolders.Count) return; if (viewHolderIndex < 0 || viewHolderIndex >= viewHolders.Count) return;
var viewHolder = viewHolders[viewHolderIndex]; var viewHolder = viewHolders[viewHolderIndex];
string viewName = viewHolder.Name;
viewHolders.RemoveAt(viewHolderIndex); viewHolders.RemoveAt(viewHolderIndex);
UnregisterViewHolder(viewHolder);
RebuildViewHolderPositions(viewHolderIndex);
Adapter?.OnRecycleViewHolder(viewHolder); Adapter?.OnRecycleViewHolder(viewHolder);
viewHolder.OnRecycled(); viewHolder.OnRecycled();
Free(viewHolder.Name, viewHolder); ClearSelectedState(viewHolder);
Free(viewName, viewHolder);
} }
} }
/// <summary>
/// 根据数据的下标获取对应的 ViewHolder
/// </summary>
/// <param name="index">数据的下标</param>
/// <returns></returns>
public ViewHolder GetViewHolder(int index) public ViewHolder GetViewHolder(int index)
{ {
foreach (var viewHolder in viewHolders) return viewHoldersByIndex.TryGetValue(index, out ViewHolder viewHolder)
{ ? viewHolder
if (viewHolder.Index == index) : null;
{
return viewHolder;
}
} }
return null; public ViewHolder GetViewHolderByDataIndex(int dataIndex)
{
return viewHoldersByDataIndex.TryGetValue(dataIndex, out List<ViewHolder> holders) &&
holders is { Count: > 0 }
? holders[0]
: null;
}
public bool TryGetViewHoldersByDataIndex(int dataIndex, out IReadOnlyList<ViewHolder> holders)
{
if (viewHoldersByDataIndex.TryGetValue(dataIndex, out List<ViewHolder> list) && list.Count > 0)
{
holders = list;
return true;
}
holders = null;
return false;
} }
/// <summary>
/// 根据数据的下标获取 ViewHolder 的下标
/// </summary>
/// <param name="index">数据的下标</param>
/// <returns></returns>
public int GetViewHolderIndex(int index) public int GetViewHolderIndex(int index)
{ {
for (int i = 0; i < viewHolders.Count; i++) return viewHolderPositions.TryGetValue(index, out int viewHolderIndex)
{ ? viewHolderIndex
if (viewHolders[i].Index == index) : -1;
{
return i;
}
} }
return -1;
}
/// <summary>
/// 清空所有 ViewHolder
/// 将所有活动的 ViewHolder 回收到对象池并清空列表
/// </summary>
public void Clear() public void Clear()
{ {
foreach (var viewHolder in viewHolders) foreach (var viewHolder in viewHolders)
{ {
string viewName = viewHolder.Name;
Adapter?.OnRecycleViewHolder(viewHolder); Adapter?.OnRecycleViewHolder(viewHolder);
UnregisterViewHolder(viewHolder);
viewHolder.OnRecycled(); viewHolder.OnRecycled();
Free(viewHolder.Name, viewHolder); ClearSelectedState(viewHolder);
Free(viewName, viewHolder);
} }
viewHolders.Clear(); viewHolders.Clear();
viewHoldersByIndex.Clear();
viewHoldersByDataIndex.Clear();
viewHolderPositions.Clear();
} }
/// <summary>
/// 计算 ViewHolder 的尺寸
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public Vector2 CalculateViewSize(int index) public Vector2 CalculateViewSize(int index)
{ {
Vector2 size = GetTemplate(Adapter.GetViewName(index)).SizeDelta; Vector2 size = GetTemplate(Adapter.GetViewName(index)).SizeDelta;
return size; return size;
} }
/// <summary>
/// 获取数据项总数
/// </summary>
/// <returns>数据项总数,如果没有适配器则返回 0</returns>
public int GetItemCount() public int GetItemCount()
{ {
return Adapter == null ? 0 : Adapter.GetItemCount(); return Adapter == null ? 0 : Adapter.GetItemCount();
} }
protected int GetRecommendedWarmCount()
{
if (Adapter == null || LayoutManager == null)
{
return 0;
}
int itemCount = Adapter.GetItemCount();
if (itemCount <= 0)
{
return 0;
}
int start = Mathf.Max(0, LayoutManager.GetStartIndex());
int end = Mathf.Max(start, LayoutManager.GetEndIndex());
int visibleCount = end - start + 1;
int bufferCount = Mathf.Max(1, LayoutManager.Unit);
return Mathf.Min(itemCount, visibleCount + bufferCount);
}
private void RegisterViewHolder(ViewHolder viewHolder)
{
if (viewHolder == null)
{
return;
}
viewHoldersByIndex[viewHolder.Index] = viewHolder;
viewHolderPositions[viewHolder.Index] = viewHolders.Count - 1;
if (!viewHoldersByDataIndex.TryGetValue(viewHolder.DataIndex, out List<ViewHolder> holders))
{
holders = new List<ViewHolder>();
viewHoldersByDataIndex[viewHolder.DataIndex] = holders;
}
holders.Add(viewHolder);
}
private void UnregisterViewHolder(ViewHolder viewHolder)
{
if (viewHolder == null)
{
return;
}
viewHoldersByIndex.Remove(viewHolder.Index);
viewHolderPositions.Remove(viewHolder.Index);
if (!viewHoldersByDataIndex.TryGetValue(viewHolder.DataIndex, out List<ViewHolder> holders))
{
return;
}
holders.Remove(viewHolder);
if (holders.Count == 0)
{
viewHoldersByDataIndex.Remove(viewHolder.DataIndex);
}
}
private void RebuildViewHolderPositions(int startIndex)
{
for (int i = startIndex; i < viewHolders.Count; i++)
{
ViewHolder holder = viewHolders[i];
if (holder != null)
{
viewHolderPositions[holder.Index] = i;
}
}
}
private static void ClearSelectedState(ViewHolder viewHolder)
{
if (viewHolder == null)
{
return;
}
EventSystem eventSystem = EventSystem.current;
if (eventSystem == null)
{
return;
}
GameObject selected = eventSystem.currentSelectedGameObject;
if (selected != null && selected.transform.IsChildOf(viewHolder.transform))
{
eventSystem.SetSelectedGameObject(null);
}
}
} }
} }

View File

@ -291,7 +291,7 @@ namespace UnityEngine.UI
} }
} }
internal void InvalidateSelectableCache() public void InvalidateSelectableCache()
{ {
_cacheDirty = true; _cacheDirty = true;
} }