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)
{
this.recyclerView = recyclerView;
this.list = list;
this.list = list ?? new List<T>();
}
public virtual int GetItemCount()
@ -68,10 +68,19 @@ namespace AlicizaX.UI
if (!TryGetBindData(index, out var data)) return;
string viewName = GetViewName(index);
Action defaultClickAction = CreateItemClickAction(index, data);
viewHolder.AdvanceBindingVersion();
viewHolder.DataIndex = index;
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);
return;
}
@ -90,24 +99,97 @@ namespace AlicizaX.UI
itemRender.UpdateSelection(false);
itemRender.Unbind();
}
viewHolder.ClearInteractionCallbacks();
}
public virtual void NotifyDataChanged()
{
CoerceChoiceIndex();
recyclerView.RequestLayout();
recyclerView.Refresh();
}
public virtual void SetList(List<T> list)
{
this.list = list;
this.list = list ?? new List<T>();
recyclerView.Reset();
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);
}
@ -166,25 +248,53 @@ namespace AlicizaX.UI
public void Add(T item)
{
if (list == null)
{
list = new List<T>();
}
list.Add(item);
NotifyDataChanged();
NotifyItemInserted(list.Count - 1);
}
public void AddRange(IEnumerable<T> collection)
{
if (collection == null)
{
return;
}
int startIndex = list.Count;
list.AddRange(collection);
if (collection is ICollection<T> itemCollection)
{
NotifyItemRangeInserted(startIndex, itemCollection.Count);
return;
}
NotifyDataChanged();
}
public void Insert(int index, T item)
{
list.Insert(index, item);
NotifyDataChanged();
NotifyItemInserted(index);
}
public void InsertRange(int index, IEnumerable<T> collection)
{
if (collection == null)
{
return;
}
list.InsertRange(index, collection);
if (collection is ICollection<T> itemCollection)
{
NotifyItemRangeInserted(index, itemCollection.Count);
return;
}
NotifyDataChanged();
}
@ -199,13 +309,13 @@ namespace AlicizaX.UI
if (index < 0 || index >= GetItemCount()) return;
list.RemoveAt(index);
NotifyDataChanged();
NotifyItemRemoved(index);
}
public void RemoveRange(int index, int count)
{
list.RemoveRange(index, count);
NotifyDataChanged();
NotifyItemRangeRemoved(index, count);
}
public void RemoveAll(Predicate<T> match)
@ -216,8 +326,14 @@ namespace AlicizaX.UI
public void Clear()
{
if (list == null || list.Count == 0)
{
return;
}
int count = list.Count;
list.Clear();
NotifyDataChanged();
NotifyItemRangeRemoved(0, count);
}
public void Reverse(int index, int count)
@ -240,6 +356,20 @@ namespace AlicizaX.UI
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 (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)
{
if (list == null || index < 0 || index >= list.Count)
@ -273,9 +397,10 @@ namespace AlicizaX.UI
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)
@ -321,6 +446,20 @@ namespace AlicizaX.UI
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)
{
if (viewHolder == null)
@ -337,7 +476,7 @@ namespace AlicizaX.UI
return true;
}
entry.ItemRender?.Unbind();
ReleaseItemRender(entry);
viewHolder.Destroyed -= OnViewHolderDestroyed;
itemRenders.Remove(viewHolder);
}
@ -348,7 +487,7 @@ namespace AlicizaX.UI
return false;
}
itemRender = definition.Create(viewHolder);
itemRender = definition.Create(viewHolder, recyclerView, this, SetChoiceIndex);
itemRenders[viewHolder] = new ItemRenderEntry(viewName, itemRender);
viewHolder.Destroyed += OnViewHolderDestroyed;
return true;
@ -363,7 +502,7 @@ namespace AlicizaX.UI
{
foreach (var pair in itemRenders)
{
pair.Value.ItemRender?.Unbind();
ReleaseItemRender(pair.Value);
if (pair.Key != null)
{
pair.Key.Destroyed -= OnViewHolderDestroyed;
@ -388,7 +527,7 @@ namespace AlicizaX.UI
continue;
}
pair.Value.ItemRender?.Unbind();
ReleaseItemRender(pair.Value);
pair.Key.Destroyed -= OnViewHolderDestroyed;
viewHoldersToRemove ??= new List<ViewHolder>();
viewHoldersToRemove.Add(pair.Key);
@ -413,7 +552,26 @@ namespace AlicizaX.UI
}
viewHolder.Destroyed -= OnViewHolderDestroyed;
itemRenders.Remove(viewHolder);
if (itemRenders.TryGetValue(viewHolder, out var entry))
{
ReleaseItemRender(entry);
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)
{
return showList[index].TemplateName;
return index >= 0 && index < showList.Count
? showList[index].TemplateName
: string.Empty;
}
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);
for (int i = 0; i < groupList.Count; i++)
if (list == null)
{
int index = showList.IndexOf(groupList[i]);
Collapse(index);
if (groupList[i].Expanded)
showList.Clear();
base.NotifyDataChanged();
return;
}
for (int i = 0; i < list.Count; i++)
{
CreateGroup(list[i].Type);
}
for (int i = 0; i < showList.Count; i++)
{
TData group = showList[i];
if (group.TemplateName != groupViewName)
{
Expand(index);
}
}
foreach (var group in groupList)
{
if (list.FindAll(data => data.Type == group.Type).Count == 0)
{
showList.Remove(group);
continue;
}
Collapse(i);
if (group.Expanded)
{
Expand(i);
i += CountItemsForType(group.Type);
}
}
RemoveEmptyGroups();
base.NotifyDataChanged();
}
@ -82,14 +94,45 @@ namespace AlicizaX.UI
public void Expand(int index)
{
var expandList = list.FindAll(data => data.Type == showList[index].Type);
showList.InsertRange(index + 1, expandList);
if (list == null || index < 0 || index >= showList.Count)
{
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)
{
var collapseList = showList.FindAll(data => data.Type == showList[index].Type && data.TemplateName != groupViewName);
showList.RemoveRange(index + 1, collapseList.Count);
if (index < 0 || index >= showList.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)
@ -104,19 +147,58 @@ namespace AlicizaX.UI
return true;
}
protected override Action CreateItemClickAction(int index, TData data)
public void Activate(int index)
{
return () =>
if (index < 0 || index >= showList.Count)
{
if (data.TemplateName == groupViewName)
return;
}
TData data = showList[index];
if (data.TemplateName == groupViewName)
{
data.Expanded = !data.Expanded;
NotifyDataChanged();
return;
}
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)
{
data.Expanded = !data.Expanded;
NotifyDataChanged();
return;
count++;
}
}
return count;
}
private void RemoveEmptyGroups()
{
for (int i = showList.Count - 1; i >= 0; i--)
{
TData group = showList[i];
if (group.TemplateName != groupViewName)
{
continue;
}
SetChoiceIndex(index);
};
if (CountItemsForType(group.Type) == 0)
{
showList.RemoveAt(i);
}
}
}
}
}

View File

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

View File

@ -1,108 +1,364 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using UnityEngine.EventSystems;
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);
/// <summary>
/// 清理当前渲染实例上的绑定状态。
/// </summary>
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
, IItemRenderInitializer
/// <summary>
/// 提供 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
{
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; }
/// <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; }
/// <summary>
/// 获取当前绑定的数据索引。
/// </summary>
protected int CurrentIndex { get; private set; } = -1;
/// <summary>
/// 获取当前绑定的布局索引。
/// </summary>
protected int CurrentLayoutIndex { get; private set; } = -1;
/// <summary>
/// 获取当前项是否处于选中状态。
/// </summary>
protected bool IsSelected { get; private set; }
public void Bind(object data, int index, Action defaultClickAction)
{
EnsureHolder();
/// <summary>
/// 获取当前绑定版本号,用于校验异步回调是否仍然有效。
/// </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)
{
throw new InvalidCastException(
$"ItemRender '{GetType().Name}' expected data '{typeof(TData).Name}', but got '{data?.GetType().Name ?? "null"}'.");
}
CurrentData = itemData;
CurrentIndex = index;
this.defaultClickAction = defaultClickAction;
Holder.SetInteractionCallbacks(HandleClick, HandlePointerEnter, HandlePointerExit);
Bind(itemData, index);
BindCore(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();
IsSelected = selected;
OnSelectionChanged(selected);
}
public void Unbind()
/// <summary>
/// 清理当前绑定数据关联的状态,并重置内部缓存。
/// </summary>
internal override void UnbindInternal()
{
ResetState();
if (Holder != null)
{
if (IsSelected)
{
IsSelected = false;
OnSelectionChanged(false);
}
OnClear();
if (interactionProxy != null)
{
interactionProxy.Clear();
interactionBindingActive = false;
cachedInteractionFlags = ItemInteractionFlags.None;
}
Holder.DataIndex = -1;
}
CurrentData = default;
CurrentIndex = -1;
CurrentLayoutIndex = -1;
CurrentBindingVersion = 0;
IsSelected = false;
}
protected abstract void Bind(TData data, int index);
/// <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;
}
protected virtual void OnHolderChanged()
/// <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)
{
}
/// <summary>
/// 每次当前数据绑定被清理时调用。
/// 这是绑定级清理生命周期,在复用过程中可能被多次触发。
/// 适合在此重置由当前绑定数据产生的临时界面状态。
/// </summary>
protected virtual void OnClear()
{
}
protected virtual void OnClick()
/// <summary>
/// 通知外部选中当前数据项。
/// </summary>
private void SelectCurrentItem()
{
if (CurrentIndex >= 0)
{
selectionHandler?.Invoke(CurrentIndex);
}
}
protected virtual void OnPointerEnter()
{
}
protected virtual void OnPointerExit()
{
}
private void HandleClick()
{
defaultClickAction?.Invoke();
OnClick();
}
private void HandlePointerEnter()
{
OnPointerEnter();
}
private void HandlePointerExit()
{
OnPointerExit();
}
void IItemRenderInitializer.Reset(ViewHolder viewHolder)
/// <summary>
/// 将当前渲染实例附加到指定持有者,并初始化上下文引用。
/// </summary>
/// <param name="viewHolder">目标视图持有者。</param>
/// <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)
{
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}'.");
}
ResetState();
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()
{
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)
{
if (IsSelected)
{
IsSelected = false;
OnSelectionChanged(false);
}
return RecyclerView != null && RecyclerView.NavigationController.TryMove(Holder, direction, NavigationOptions);
}
OnClear();
Holder.ClearInteractionCallbacks();
/// <summary>
/// 在需要时将当前渲染实例绑定到交互代理。
/// </summary>
private void BindInteractionProxyIfNeeded()
{
if (interactionProxy == null)
{
return;
}
CurrentData = default;
CurrentIndex = -1;
IsSelected = false;
defaultClickAction = null;
ItemInteractionFlags interactionFlags = InteractionFlags;
if (interactionBindingActive && cachedInteractionFlags == interactionFlags)
{
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
{
/// <summary>
/// 描述单个 ItemRender 的类型信息与创建方式。
/// </summary>
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;
HolderType = holderType;
this.createInstance = createInstance;
}
/// <summary>
/// 获取渲染器运行时类型。
/// </summary>
public Type ItemRenderType { get; }
/// <summary>
/// 获取渲染器要求的持有者类型。
/// </summary>
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)
{
@ -177,25 +703,27 @@ namespace AlicizaX.UI
$"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(
$"RecyclerView item render '{ItemRenderType.FullName}' could not be created.");
}
if (itemRender is not IItemRenderInitializer initializer)
{
throw new InvalidOperationException(
$"RecyclerView item render '{ItemRenderType.FullName}' must inherit from ItemRender<TData, THolder>.");
}
initializer.Reset(viewHolder);
itemRender.Attach(viewHolder, recyclerView, adapter, selectionHandler);
return itemRender;
}
}
/// <summary>
/// ItemRender 定义缓存表,键为渲染器类型。
/// </summary>
private static readonly Dictionary<Type, ItemRenderDefinition> Definitions = new();
/// <summary>
/// 获取指定渲染器类型对应的定义,不存在时自动创建并缓存。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <returns>与该类型对应的渲染器定义。</returns>
public static ItemRenderDefinition GetOrCreate(Type itemRenderType)
{
if (itemRenderType == null)
@ -213,6 +741,11 @@ namespace AlicizaX.UI
return definition;
}
/// <summary>
/// 为指定渲染器类型构建定义信息。
/// </summary>
/// <param name="itemRenderType">渲染器运行时类型。</param>
/// <returns>创建完成的渲染器定义。</returns>
private static ItemRenderDefinition CreateDefinition(Type itemRenderType)
{
if (itemRenderType.IsAbstract ||
@ -242,9 +775,15 @@ namespace AlicizaX.UI
$"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)
{
for (Type current = itemRenderType; current != null && current != typeof(object); current = current.BaseType)
@ -261,5 +800,17 @@ namespace AlicizaX.UI
holderType = null;
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()
{
return int.MaxValue;
return GetRealCount() > 0 ? int.MaxValue : 0;
}
public override int GetRealCount()
@ -24,6 +24,11 @@ namespace AlicizaX.UI
public override void OnBindViewHolder(ViewHolder viewHolder, int index)
{
if (list == null || list.Count == 0)
{
return;
}
index %= list.Count;
base.OnBindViewHolder(viewHolder, index);
}

View File

@ -14,7 +14,9 @@ namespace AlicizaX.UI
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
{
/// <summary>
/// 正弦缓入函数
/// </summary>
public static double EaseInSine(float x)
{
return 1 - Math.Cos(x * Math.PI / 2);
}
/// <summary>
/// 正弦缓出函数
/// </summary>
public static double EaseOutSine(float x)
{
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
guid: b174ccb64b3938c449d4a69a3262d8d5
guid: b993c99fa9bf9634a8eb949a82efe103
MonoImporter:
externalObjects: {}
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
guid: 491a09d781095104989abb7a91424008
guid: 264e45e52d936c44b96c6bb5eeaf4b98
MonoImporter:
externalObjects: {}
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;
namespace AlicizaX.UI
@ -21,9 +20,17 @@ namespace AlicizaX.UI
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);
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;
}
@ -62,7 +69,7 @@ namespace AlicizaX.UI
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;
@ -77,6 +84,11 @@ namespace AlicizaX.UI
public override float IndexToPosition(int index)
{
if (Mathf.Approximately(intervalAngle, 0f))
{
return 0f;
}
float position = index * intervalAngle;
return -position;
@ -84,13 +96,18 @@ namespace AlicizaX.UI
public override int PositionToIndex(float position)
{
if (Mathf.Approximately(intervalAngle, 0f))
{
return 0;
}
int index = Mathf.RoundToInt(position / intervalAngle);
return -index;
}
public override void DoItemAnimation()
{
List<ViewHolder> viewHolders = viewProvider.ViewHolders;
var viewHolders = viewProvider.ViewHolders;
for (int i = 0; i < viewHolders.Count; i++)
{
float angle = i * intervalAngle + initalAngle;

View File

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

View File

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

View File

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

View File

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

View File

@ -6,133 +6,238 @@ namespace AlicizaX.UI
[Serializable]
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 override Vector2 CalculateContentSize()
{
int index = adapter.GetItemCount();
float position = 0;
for (int i = 0; i < index; i++)
{
position += GetLength(i);
}
positionCacheDirty = true;
EnsurePositionCache();
return direction == Direction.Vertical ?
new Vector2(contentSize.x, position - spacing.y + padding.y * 2) :
new Vector2(position - spacing.x + padding.x * 2, contentSize.y);
float totalLength = cachedItemCount > 0
? itemPositions[cachedItemCount - 1] + itemLengths[cachedItemCount - 1]
: 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)
{
// TODO 优化点,将 position 定义成全局变量
float position = 0;
for (int i = 0; i < index; i++)
{
position += GetLength(i);
}
position -= ScrollPosition;
return direction == Direction.Vertical ? new Vector2(0, position + padding.y) : new Vector2(position + padding.x, 0);
EnsurePositionCache();
float position = GetItemPosition(index) - ScrollPosition;
return direction == Direction.Vertical
? new Vector2(0, position + padding.y)
: new Vector2(position + padding.x, 0);
}
public override Vector2 CalculateContentOffset()
{
Vector2 size = viewProvider.CalculateViewSize(0);
EnsurePositionCache();
if (cachedItemCount <= 0)
{
return Vector2.zero;
}
float len = GetFitContentSize();
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()
{
Vector2 size = viewProvider.CalculateViewSize(0);
EnsurePositionCache();
if (cachedItemCount <= 0)
{
return Vector2.zero;
}
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()
{
float position = 0;
float contentPosition = ScrollPosition;
int itemCount = adapter.GetItemCount();
for (int i = 0; i < itemCount; i++)
EnsurePositionCache();
if (cachedItemCount <= 0)
{
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()
{
float position = 0;
EnsurePositionCache();
if (cachedItemCount <= 0)
{
return -1;
}
float viewLength = direction == Direction.Vertical ? viewportSize.y : viewportSize.x;
int itemCount = adapter.GetItemCount();
for (int i = 0; i < itemCount; i++)
{
position += GetLength(i);
if (position > ScrollPosition + viewLength)
{
return Mathf.Min(i, adapter.GetItemCount() - 1); ;
}
}
return itemCount - 1;
}
private float GetLength(int index)
{
Vector2 size = viewProvider.CalculateViewSize(index);
if (index < adapter.GetItemCount() - 1)
{
size += spacing;
}
float len = direction == Direction.Vertical ? size.y : size.x;
return len;
int index = FindFirstItemEndingAfter(ScrollPosition + viewLength);
return index >= 0 ? Mathf.Min(index, cachedItemCount - 1) : cachedItemCount - 1;
}
public override float IndexToPosition(int index)
{
Vector2 position = CalculatePosition(index);
EnsurePositionCache();
if (cachedItemCount <= 0)
{
return 0f;
}
float position = GetItemPosition(index);
if (direction == Direction.Vertical)
{
position.y = Mathf.Max(0, position.y);
position.y = Mathf.Min(position.y, contentSize.y - viewportSize.y);
return position.y;
}
else
{
position.x = Mathf.Max(0, position.x);
position.x = Mathf.Min(position.x, contentSize.x - viewportSize.x);
return position.x;
return Mathf.Clamp(position, 0f, Mathf.Max(contentSize.y - viewportSize.y, 0f));
}
return Mathf.Clamp(position, 0f, Mathf.Max(contentSize.x - viewportSize.x, 0f));
}
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++)
{
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)
{
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)
{
return i;
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.Collections.Generic;
using System;
using UnityEngine;
namespace AlicizaX.UI
@ -15,19 +14,27 @@ namespace AlicizaX.UI
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);
lineHeight = direction == Direction.Vertical ? size.y : size.x;
int index = adapter.GetItemCount();
float position;
if (direction == Direction.Vertical)
{
position = index * (lineHeight + spacing.y) - spacing.y;
position = itemCount * (lineHeight + spacing.y) - spacing.y;
position += viewportSize.y - lineHeight;
return new Vector2(contentSize.x, position + padding.y * 2);
}
position = index * (lineHeight + spacing.x) - spacing.x;
position = itemCount * (lineHeight + spacing.x) - spacing.x;
position += viewportSize.x - lineHeight;
return new Vector2(position + padding.x * 2, contentSize.y);
}
@ -57,15 +64,29 @@ namespace AlicizaX.UI
protected override float GetOffset()
{
if (lineHeight <= 0f)
{
return 0f;
}
float offset = direction == Direction.Vertical ? viewportSize.y - lineHeight : viewportSize.x - lineHeight;
return offset / 2;
}
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;
if (len <= 0f)
{
return 0;
}
float pos = IndexToPosition(recyclerView.CurrentIndex);
// 根据是前划还是后划,来加减偏移量
int index = position > pos ? Mathf.RoundToInt(position / len + 0.25f) : Mathf.RoundToInt(position / len - 0.25f);
return index;
@ -73,7 +94,7 @@ namespace AlicizaX.UI
public override void DoItemAnimation()
{
List<ViewHolder> viewHolders = viewProvider.ViewHolders;
var viewHolders = viewProvider.ViewHolders;
for (int i = 0; i < viewHolders.Count; i++)
{
float viewPos = direction == Direction.Vertical ? -viewHolders[i].RectTransform.anchoredPosition.y : viewHolders[i].RectTransform.anchoredPosition.x;

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
{
/// <summary>
/// 创建对象
/// </summary>
/// <returns></returns>
T Create();
/// <summary>
/// 销毁对象
/// </summary>
/// <param name="obj"></param>
void Destroy(T obj);
/// <summary>
/// 重置对象
/// </summary>
/// <param name="obj"></param>
void Reset(T obj);
/// <summary>
/// 验证对象
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
bool Validate(T obj);
}
}

View File

@ -4,16 +4,8 @@ namespace AlicizaX.UI
public interface IObjectPool : IDisposable
{
/// <summary>
/// 从池子中分配一个可用对象,没有的话就创建一个
/// </summary>
/// <returns></returns>
object Allocate();
/// <summary>
/// 将对象回收到池子中去,如果池中的对象数量已经超过了 maxSize则直接销毁该对象
/// </summary>
/// <param name="obj"></param>
void Free(object obj);
}
@ -23,5 +15,4 @@ namespace AlicizaX.UI
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
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
public class MixedObjectPool<T> : IMixedObjectPool<T> where T : class
{
private const int DEFAULT_MAX_SIZE_PER_TYPE = 10;
private readonly ConcurrentDictionary<string, List<T>> entries;
private readonly ConcurrentDictionary<string, int> typeSize;
private readonly Dictionary<string, Stack<T>> entries;
private readonly Dictionary<string, int> typeSize;
private readonly Dictionary<string, int> activeCountByType;
private readonly Dictionary<string, int> peakActiveByType;
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)
{
@ -28,19 +32,25 @@ namespace AlicizaX.UI
throw new ArgumentException("The maxSize must be greater than 0.");
}
entries = new ConcurrentDictionary<string, List<T>>();
typeSize = new ConcurrentDictionary<string, int>();
entries = new Dictionary<string, Stack<T>>(StringComparer.Ordinal);
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)
{
if (entries.TryGetValue(typeName, out List<T> list) && list.Count > 0)
Stack<T> stack = GetOrCreateStack(typeName);
if (stack.Count > 0)
{
T obj = list[0];
list.RemoveAt(0);
T obj = stack.Pop();
hitCount++;
TrackAllocate(typeName);
return obj;
}
missCount++;
TrackAllocate(typeName);
return factory.Create(typeName);
}
@ -51,19 +61,25 @@ namespace AlicizaX.UI
if (!factory.Validate(typeName, obj))
{
factory.Destroy(typeName, obj);
destroyCount++;
TrackFree(typeName);
return;
}
int maxSize = GetMaxSize(typeName);
List<T> list = entries.GetOrAdd(typeName, n => new List<T>());
if (list.Count >= maxSize)
Stack<T> stack = GetOrCreateStack(typeName);
factory.Reset(typeName, obj);
TrackFree(typeName);
if (stack.Count >= maxSize)
{
factory.Destroy(typeName, obj);
destroyCount++;
return;
}
factory.Reset(typeName, obj);
list.Add(obj);
stack.Push(obj);
}
public int GetMaxSize(string typeName)
@ -78,24 +94,79 @@ namespace AlicizaX.UI
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()
{
foreach (var kv in entries)
{
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));
list.Clear();
while (stack.Count > 0)
{
factory.Destroy(typeName, stack.Pop());
destroyCount++;
}
}
entries.Clear();
typeSize.Clear();
activeCountByType.Clear();
peakActiveByType.Clear();
}
public void Dispose()
@ -103,6 +174,42 @@ namespace AlicizaX.UI
Clear();
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
{
using System;
using System.Threading;
using System.Collections.Generic;
public class ObjectPool<T> : IObjectPool<T> where T : class
{
private readonly Stack<T> entries;
private readonly int initialSize;
private int maxSize;
private int initialSize;
protected readonly T[] entries = null;
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)
{
@ -23,37 +29,62 @@ namespace AlicizaX.UI
this.factory = factory;
this.initialSize = initialSize;
this.maxSize = maxSize;
this.entries = new T[maxSize];
if (maxSize < initialSize)
{
throw new ArgumentException("The maxSize must be greater than or equal to the initialSize.");
}
for (int i = 0; i < initialSize; i++)
{
entries[i] = factory.Create();
}
entries = new Stack<T>(maxSize);
Warm(initialSize);
}
public int MaxSize => maxSize;
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()
{
for (var i = 0; i < entries.Length; i++)
T value;
if (entries.Count > 0)
{
T value = entries[i];
if (value == null) continue;
if (Interlocked.CompareExchange(ref entries[i], null, value) == value)
value = entries.Pop();
if (value != null)
{
hitCount++;
activeCount++;
if (activeCount > peakActive)
{
peakActive = activeCount;
}
return value;
}
}
return factory.Create();
missCount++;
value = factory.Create();
totalCount++;
activeCount++;
if (activeCount > peakActive)
{
peakActive = activeCount;
}
return value;
}
public virtual void Free(T obj)
@ -63,20 +94,39 @@ namespace AlicizaX.UI
if (!factory.Validate(obj))
{
factory.Destroy(obj);
destroyCount++;
if (totalCount > 0)
{
totalCount--;
}
if (activeCount > 0)
{
activeCount--;
}
return;
}
factory.Reset(obj);
for (var i = 0; i < entries.Length; i++)
if (activeCount > 0)
{
if (Interlocked.CompareExchange(ref entries[i], obj, null) == null)
{
return;
}
activeCount--;
}
if (entries.Count < maxSize)
{
entries.Push(obj);
return;
}
factory.Destroy(obj);
destroyCount++;
if (totalCount > 0)
{
totalCount--;
}
}
object IObjectPool.Allocate()
@ -91,15 +141,17 @@ namespace AlicizaX.UI
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)
{
factory.Destroy(value);
destroyCount++;
}
}
totalCount = activeCount;
}
public void Dispose()
@ -107,6 +159,38 @@ namespace AlicizaX.UI
Clear();
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)
{
obj.gameObject.SetActive(false);
obj.gameObject.transform.position = Vector3.zero;
obj.gameObject.transform.rotation = Quaternion.identity;
}
public bool Validate(T obj)

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,4 +1,4 @@
using System.Collections;
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
@ -6,6 +6,9 @@ namespace AlicizaX.UI
{
public class Scroller : MonoBehaviour, IScroller, IBeginDragHandler, IEndDragHandler, IDragHandler, IScrollHandler
{
private static readonly WaitForEndOfFrame EndOfFrameYield = new();
private Coroutine movementCoroutine;
protected float position;
public float Position { get => position; set => position = value; }
@ -19,9 +22,6 @@ namespace AlicizaX.UI
set => direction = value;
}
/// <summary>
/// 内容所需要大小
/// </summary>
protected Vector2 contentSize;
public Vector2 ContentSize
{
@ -29,9 +29,6 @@ namespace AlicizaX.UI
set => contentSize = value;
}
/// <summary>
/// 所在 View 的真实大小
/// </summary>
protected Vector2 viewSize;
public Vector2 ViewSize
{
@ -76,12 +73,14 @@ namespace AlicizaX.UI
public DraggingEvent OnDragging { get => draggingEvent; set => draggingEvent = value; }
// 停止滑动的时间,但此时并未释放鼠标按键
public float dragStopTime = 0f;
public virtual void ScrollTo(float position, bool smooth = false)
{
if (position == this.position) return;
if (Mathf.Approximately(position, this.position)) return;
StopMovement();
if (!smooth)
{
@ -90,11 +89,28 @@ namespace AlicizaX.UI
}
else
{
StopAllCoroutines();
StartCoroutine(MoveTo(position));
movementCoroutine = StartCoroutine(RunMotion(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)
{
ScrollTo(MaxPosition * ratio, false);
@ -103,7 +119,7 @@ namespace AlicizaX.UI
public void OnBeginDrag(PointerEventData eventData)
{
OnDragging?.Invoke(true);
StopAllCoroutines();
StopMovement();
}
public void OnEndDrag(PointerEventData eventData)
@ -125,7 +141,7 @@ namespace AlicizaX.UI
public void OnScroll(PointerEventData eventData)
{
StopAllCoroutines();
StopMovement();
float rate = GetScrollRate() * wheelSpeed;
velocity = direction == Direction.Vertical ? -eventData.scrollDelta.y * rate : eventData.scrollDelta.x * rate;
@ -156,18 +172,13 @@ namespace AlicizaX.UI
return rate;
}
/// <summary>
/// 松手时的惯性滑动
/// </summary>
protected virtual void Inertia()
{
// 松手时的时间 离 停止滑动的时间 超过一定时间,则认为此次惯性滑动无效
if (!snap && (Time.time - dragStopTime) > 0.01f) return;
if (Mathf.Abs(velocity) > 0.1f)
{
StopAllCoroutines();
StartCoroutine(InertiaTo());
StopMovement();
movementCoroutine = StartCoroutine(RunMotion(InertiaTo()));
}
else
{
@ -175,20 +186,17 @@ namespace AlicizaX.UI
}
}
/// <summary>
/// 滑动到顶部/底部之后,松手时回弹
/// </summary>
protected virtual void Elastic()
{
if (position < 0)
{
StopAllCoroutines();
StartCoroutine(ElasticTo(0));
StopMovement();
movementCoroutine = StartCoroutine(RunMotion(ElasticTo(0)));
}
else if (position > MaxPosition)
{
StopAllCoroutines();
StartCoroutine(ElasticTo(MaxPosition));
StopMovement();
movementCoroutine = StartCoroutine(RunMotion(ElasticTo(MaxPosition)));
}
}
@ -208,7 +216,7 @@ namespace AlicizaX.UI
OnValueChanged?.Invoke(position);
yield return new WaitForEndOfFrame();
yield return EndOfFrameYield;
}
OnMoveStoped?.Invoke();
@ -224,6 +232,24 @@ namespace AlicizaX.UI
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)
{
float startPos = position;
@ -235,11 +261,28 @@ namespace AlicizaX.UI
time += Time.deltaTime;
yield return new WaitForEndOfFrame();
yield return EndOfFrameYield;
}
position = targetPos;
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
{
/// <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
{
/// <summary>
/// 关联的 RecyclerView 实例。
/// </summary>
protected readonly RecyclerView _recyclerView;
/// <summary>
/// 当前列表使用的适配器实例。
/// </summary>
protected readonly TAdapter _adapter;
/// <summary>
/// 获取当前绑定的 RecyclerView。
/// </summary>
public RecyclerView RecyclerView => _recyclerView;
/// <summary>
/// 初始化列表封装并将适配器绑定到 RecyclerView。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
/// <param name="adapter">用于驱动列表渲染的适配器。</param>
public UGListBase(RecyclerView recyclerView, TAdapter adapter)
{
_recyclerView = recyclerView;
@ -21,30 +41,60 @@ namespace AlicizaX.UI
}
}
/// <summary>
/// 获取当前列表使用的适配器。
/// </summary>
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);
}
/// <summary>
/// 按运行时类型注册指定视图对应的 ItemRender。
/// </summary>
/// <param name="itemRenderType">ItemRender 的运行时类型。</param>
/// <param name="viewName">视图名称;为空时表示默认视图。</param>
public void RegisterItemRender(Type itemRenderType, string viewName = "")
{
_adapter.RegisterItemRender(itemRenderType, viewName);
}
/// <summary>
/// 注销指定视图名称对应的 ItemRender 注册。
/// </summary>
/// <param name="viewName">视图名称;为空时表示默认视图。</param>
/// <returns>是否成功移除对应注册。</returns>
public bool UnregisterItemRender(string viewName = "")
{
return _adapter.UnregisterItemRender(viewName);
}
/// <summary>
/// 清空当前列表的全部 ItemRender 注册信息。
/// </summary>
public void ClearItemRenderRegistrations()
{
_adapter.ClearItemRenderRegistrations();
}
/// <summary>
/// 当前持有的数据集合引用。
/// </summary>
private List<TData> _datas;
/// <summary>
/// 获取或设置当前列表数据。
/// </summary>
/// <remarks>
/// 设置数据时会同步调用适配器刷新列表内容。
/// </remarks>
public List<TData> Data
{
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
{
/// <summary>
/// 初始化单模板列表。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
public UGList(RecyclerView 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()
{
/// <summary>
/// 初始化分组列表。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
/// <param name="groupViewName">分组头使用的模板名称。</param>
public UGGroupList(RecyclerView recyclerView, string 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()
{
/// <summary>
/// 初始化循环列表。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
public UGLoopList(RecyclerView recyclerView)
: base(recyclerView, new LoopAdapter<TData>(recyclerView))
{
}
}
/// <summary>
/// 提供多模板列表的便捷封装。
/// </summary>
/// <typeparam name="TData">多模板列表数据类型。</typeparam>
public class UGMixedList<TData> : UGListBase<TData, MixedAdapter<TData>> where TData : IMixedViewData
{
/// <summary>
/// 初始化多模板列表。
/// </summary>
/// <param name="recyclerView">目标 RecyclerView。</param>
public UGMixedList(RecyclerView recyclerView)
: base(recyclerView, new MixedAdapter<TData>(recyclerView))
{
}
}
/// <summary>
/// 提供常用 UGList 类型的快速创建方法。
/// </summary>
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
=> 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()
=> 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()
=> 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
=> new UGMixedList<TData>(recyclerView);
}

View File

@ -1,28 +1,29 @@
using System;
using System;
using UnityEngine;
namespace AlicizaX.UI
{
/// <summary>
/// UGList 扩展方法类
/// 提供增强的滚动功能
/// 提供 UGList 的常用扩展方法。
/// </summary>
public static class UGListExtensions
{
/// <summary>
/// 启用 ScrollTo 操作的调试日志
/// 控制是否输出滚动定位调试日志。
/// </summary>
public static bool DebugScrollTo { get; set; } = false;
/// <summary>
/// 滚动到指定的列表项,支持对齐方式和动画选项
/// 将列表滚动到指定索引,并按给定对齐方式定位。
/// </summary>
/// <param name="ugList">UGList 实例</param>
/// <param name="index">要滚动到的列表项索引</param>
/// <param name="alignment">列表项在视口中的对齐方式(起始、居中或结束)</param>
/// <param name="offset">对齐后额外应用的偏移量(像素)</param>
/// <param name="smooth">是否使用动画滚动</param>
/// <param name="duration">动画持续时间(秒),仅在 smooth 为 true 时使用</param>
/// <typeparam name="TData">列表数据类型。</typeparam>
/// <typeparam name="TAdapter">适配器类型。</typeparam>
/// <param name="ugList">目标列表实例。</param>
/// <param name="index">目标数据索引。</param>
/// <param name="alignment">滚动完成后的对齐方式。</param>
/// <param name="offset">在对齐基础上的额外偏移量。</param>
/// <param name="smooth">是否使用平滑滚动。</param>
/// <param name="duration">平滑滚动时长,单位为秒。</param>
public static void ScrollTo<TData, TAdapter>(
this UGListBase<TData, TAdapter> ugList,
int index,
@ -48,8 +49,15 @@ namespace AlicizaX.UI
}
/// <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>(
this UGListBase<TData, TAdapter> ugList,
int index,
@ -63,8 +71,15 @@ namespace AlicizaX.UI
}
/// <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>(
this UGListBase<TData, TAdapter> ugList,
int index,
@ -78,8 +93,15 @@ namespace AlicizaX.UI
}
/// <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>(
this UGListBase<TData, TAdapter> ugList,
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
{
private RectTransform rectTransform;
private Action clickAction;
private Action pointerEnterAction;
private Action pointerExitAction;
internal event Action<ViewHolder> Destroyed;
@ -23,53 +20,33 @@ namespace AlicizaX.UI
return rectTransform;
}
private set => rectTransform = value;
}
public string Name { 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;
internal uint AdvanceBindingVersion()
{
BindingVersion = BindingVersion == uint.MaxValue ? 1u : BindingVersion + 1u;
return BindingVersion;
}
protected internal virtual void OnRecycled()
{
}
internal void SetInteractionCallbacks(
Action clickAction = null,
Action pointerEnterAction = 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();
AdvanceBindingVersion();
Name = string.Empty;
Index = -1;
DataIndex = -1;
RecyclerView = null;
}
protected virtual void OnDestroy()

View File

@ -1,26 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AlicizaX.UI
{
public class MixedViewProvider : ViewProvider
{
[SerializeField] private ViewHolder chatLeftViewHolder;
[SerializeField] private ViewHolder chatRightViewHolder;
private readonly MixedObjectPool<ViewHolder> objectPool;
private readonly Dictionary<string, ViewHolder> templatesByName = new(StringComparer.Ordinal);
private IMixedObjectPool<ViewHolder> objectPool;
private Dictionary<string, ViewHolder> dict = new();
public override string PoolStats =>
$"hits={objectPool.HitCount}, misses={objectPool.MissCount}, destroys={objectPool.DestroyCount}";
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;
}
templatesByName[template.name] = template;
}
UnityMixedComponentFactory<ViewHolder> factory = new(dict, recyclerView.Content);
UnityMixedComponentFactory<ViewHolder> factory = new(templatesByName, recyclerView.Content);
objectPool = new MixedObjectPool<ViewHolder>(factory);
}
@ -30,7 +34,13 @@ namespace AlicizaX.UI
{
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()
@ -39,7 +49,10 @@ namespace AlicizaX.UI
{
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)
@ -60,5 +73,38 @@ namespace AlicizaX.UI
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
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
{
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)
{
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 = "")
@ -48,5 +51,17 @@ namespace AlicizaX.UI
(Adapter as IItemRenderCacheOwner)?.ReleaseAllItemRenders();
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 UnityEngine;
using UnityEngine.EventSystems;
namespace AlicizaX.UI
{
/// <summary>
/// 提供和管理 ViewHolder
/// 负责 ViewHolder 的创建、回收和复用
/// </summary>
public abstract class ViewProvider
{
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; }
/// <summary>
/// 获取或设置布局管理器
/// </summary>
public LayoutManager LayoutManager { get; set; }
/// <summary>
/// 获取当前所有活动的 ViewHolder 列表
/// </summary>
public List<ViewHolder> ViewHolders => viewHolders;
public IReadOnlyList<ViewHolder> ViewHolders => viewHolders;
public abstract string PoolStats { get; }
protected RecyclerView recyclerView;
protected ViewHolder[] templates;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="recyclerView">关联的 RecyclerView 实例</param>
/// <param name="templates">ViewHolder 模板数组</param>
public ViewProvider(RecyclerView recyclerView, ViewHolder[] templates)
{
this.recyclerView = recyclerView;
this.templates = templates;
}
/// <summary>
/// 根据视图名称获取对应的模板(抽象方法,由子类实现)
/// </summary>
/// <param name="viewName">视图名称</param>
/// <returns>对应的 ViewHolder 模板</returns>
public abstract ViewHolder GetTemplate(string viewName);
/// <summary>
/// 获取所有模板(抽象方法,由子类实现)
/// </summary>
/// <returns>所有 ViewHolder 模板数组</returns>
public abstract ViewHolder[] GetTemplates();
/// <summary>
/// 从对象池中分配一个 ViewHolder抽象方法由子类实现
/// </summary>
/// <param name="viewName">视图名称</param>
/// <returns>分配的 ViewHolder 实例</returns>
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);
/// <summary>
/// 重置 ViewProvider 状态(抽象方法,由子类实现)
/// </summary>
public abstract void Reset();
/// <summary>
/// 创建指定索引的 ViewHolder
/// 从对象池中获取或创建新的 ViewHolder并进行布局和数据绑定
/// </summary>
/// <param name="index">数据索引</param>
public abstract void PreparePool();
public void CreateViewHolder(int index)
{
for (int i = index; i < index + LayoutManager.Unit; i++)
@ -87,18 +50,16 @@ namespace AlicizaX.UI
var viewHolder = Allocate(viewName);
viewHolder.Name = viewName;
viewHolder.Index = i;
viewHolder.DataIndex = i;
viewHolder.RecyclerView = recyclerView;
viewHolders.Add(viewHolder);
RegisterViewHolder(viewHolder);
LayoutManager.Layout(viewHolder, i);
Adapter.OnBindViewHolder(viewHolder, i);
}
}
/// <summary>
/// 移除指定索引的 ViewHolder
/// 将 ViewHolder 从活动列表中移除并回收到对象池
/// </summary>
/// <param name="index">数据索引</param>
public void RemoveViewHolder(int index)
{
for (int i = index; i < index + LayoutManager.Unit; i++)
@ -110,83 +71,171 @@ namespace AlicizaX.UI
if (viewHolderIndex < 0 || viewHolderIndex >= viewHolders.Count) return;
var viewHolder = viewHolders[viewHolderIndex];
string viewName = viewHolder.Name;
viewHolders.RemoveAt(viewHolderIndex);
UnregisterViewHolder(viewHolder);
RebuildViewHolderPositions(viewHolderIndex);
Adapter?.OnRecycleViewHolder(viewHolder);
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)
{
foreach (var viewHolder in viewHolders)
{
if (viewHolder.Index == index)
{
return viewHolder;
}
}
return null;
return viewHoldersByIndex.TryGetValue(index, out ViewHolder viewHolder)
? viewHolder
: 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)
{
for (int i = 0; i < viewHolders.Count; i++)
{
if (viewHolders[i].Index == index)
{
return i;
}
}
return -1;
return viewHolderPositions.TryGetValue(index, out int viewHolderIndex)
? viewHolderIndex
: -1;
}
/// <summary>
/// 清空所有 ViewHolder
/// 将所有活动的 ViewHolder 回收到对象池并清空列表
/// </summary>
public void Clear()
{
foreach (var viewHolder in viewHolders)
{
string viewName = viewHolder.Name;
Adapter?.OnRecycleViewHolder(viewHolder);
UnregisterViewHolder(viewHolder);
viewHolder.OnRecycled();
Free(viewHolder.Name, viewHolder);
ClearSelectedState(viewHolder);
Free(viewName, viewHolder);
}
viewHolders.Clear();
viewHoldersByIndex.Clear();
viewHoldersByDataIndex.Clear();
viewHolderPositions.Clear();
}
/// <summary>
/// 计算 ViewHolder 的尺寸
/// </summary>
/// <param name="index"></param>
/// <returns></returns>
public Vector2 CalculateViewSize(int index)
{
Vector2 size = GetTemplate(Adapter.GetViewName(index)).SizeDelta;
return size;
}
/// <summary>
/// 获取数据项总数
/// </summary>
/// <returns>数据项总数,如果没有适配器则返回 0</returns>
public int 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;
}