RecyclerView 大优化
优化RecycleView 渲染架构 优化RecyclerView 渲染性能 增加 缓存 异步 增加Navagation导航锚点相关
This commit is contained in:
parent
0ed273ba9e
commit
dc8c840d69
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
8
Runtime/RecyclerView/Interaction.meta
Normal file
8
Runtime/RecyclerView/Interaction.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e63a2384482cb7b418bc1a4149b11742
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
31
Runtime/RecyclerView/Interaction/IItemInteractionHost.cs
Normal file
31
Runtime/RecyclerView/Interaction/IItemInteractionHost.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b174ccb64b3938c449d4a69a3262d8d5
|
||||
guid: b993c99fa9bf9634a8eb949a82efe103
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
21
Runtime/RecyclerView/Interaction/ItemInteractionFlags.cs
Normal file
21
Runtime/RecyclerView/Interaction/ItemInteractionFlags.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 491a09d781095104989abb7a91424008
|
||||
guid: 264e45e52d936c44b96c6bb5eeaf4b98
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
205
Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs
Normal file
205
Runtime/RecyclerView/Interaction/ItemInteractionProxy.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac4f0b81367e72b408b7d4a0148d39c3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Runtime/RecyclerView/Interaction/RecyclerItemSelectable.cs
Normal file
16
Runtime/RecyclerView/Interaction/RecyclerItemSelectable.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 720e8e459a50e2443847a385966fd104
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
8
Runtime/RecyclerView/Navigation.meta
Normal file
8
Runtime/RecyclerView/Navigation.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2450ac660a373c24caa2f5eba25c8237
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
49
Runtime/RecyclerView/Navigation/RecyclerNavigationBridge.cs
Normal file
49
Runtime/RecyclerView/Navigation/RecyclerNavigationBridge.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eaec44cc09df6a546a8b02169d0a3e18
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f3d534ccefdd51448ffa7281ae6d881
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
21
Runtime/RecyclerView/Navigation/RecyclerNavigationOptions.cs
Normal file
21
Runtime/RecyclerView/Navigation/RecyclerNavigationOptions.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a92798bee41f6334ba18374359da1329
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
public interface IPooledObject
|
||||
{
|
||||
void Free();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
@ -1,23 +1,9 @@
|
||||
namespace AlicizaX.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 定义滚动到列表项时的对齐方式
|
||||
/// </summary>
|
||||
public enum ScrollAlignment
|
||||
{
|
||||
/// <summary>
|
||||
/// 将列表项对齐到视口的顶部/左侧
|
||||
/// </summary>
|
||||
Start,
|
||||
|
||||
/// <summary>
|
||||
/// 将列表项对齐到视口的中心
|
||||
/// </summary>
|
||||
Center,
|
||||
|
||||
/// <summary>
|
||||
/// 将列表项对齐到视口的底部/右侧
|
||||
/// </summary>
|
||||
End
|
||||
}
|
||||
}
|
||||
|
||||
@ -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> { }
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,7 +291,7 @@ namespace UnityEngine.UI
|
||||
}
|
||||
}
|
||||
|
||||
internal void InvalidateSelectableCache()
|
||||
public void InvalidateSelectableCache()
|
||||
{
|
||||
_cacheDirty = true;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user