89 KiB
RecyclerView 专业手册
📌 本文档基于
Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView的运行时 API 编写,重点说明业务侧可直接调用的能力;文中提到的private/internal成员仅用于解释内部机制,不属于业务层 API。⚠️
SetAdapter()、Reset()、Refresh()、RequestLayout()已收敛为框架内部流程;业务层请通过UGList、UGListCreateHelper、UGList.Data、Adapter.SetList()、Notify*()等正式入口驱动列表更新,不要手动调用这些底层方法。所有 UI 相关调用仍应统一放在 Unity 主线程。💡
RecyclerView的LayoutManager与Scroller通过 Inspector 序列化引用配置,业务代码通常只“读取与使用”,不建议在运行中直接替换类型。
目录
- 模块概览
- 基础概念与共享示例类型
- 容器(RecyclerView)
- 适配器(Adapter / GroupAdapter / LoopAdapter / MixedAdapter)
- 布局(LayoutManager 系列)
- 滚动器(Scroller / CircleScroller)
- 视图池(ViewProvider / ObjectPool)
- 导航(RecyclerNavigation)
- 业务包装层(UGList / UGListExtensions)
- 场景专题
- FAQ
- Anti-patterns
- 性能优化建议
- 交付前检查清单
模块概览
RecyclerView 是一套面向 Unity UGUI 的高性能列表组件,职责拆分如下:
- 容器:
RecyclerView负责模板、可见区、滚动同步、滚动条、焦点恢复。 - 适配器:
Adapter<T>家族负责数据量、模板选择、视图绑定、局部刷新。 - 布局:
LayoutManager家族负责位置计算、可见区间与索引转换。 - 滚动器:
Scroller家族负责拖拽、滚轮、惯性、吸附与平滑滚动。 - 视图池:
ViewProvider+ObjectPool负责创建、回收、预热与统计。 - 导航:
RecyclerNavigationController家族负责手柄/键盘在列表内的可达性。 - 业务包装层:
UGList家族负责降低泛型与注册样板代码。
💡 一般调用顺序是:准备模板与布局 → 创建业务包装层/适配器包装 → 注册
ItemRender→ 赋值数据;后续布局计算、池预热与首屏刷新由框架自动完成。
⚠️ 如果
Templates为空、LayoutManager未配置或Content解析失败,RecyclerView会在首次运行时抛出错误或记录错误日志。
基础概念与共享示例类型
本节给出后续示例默认共用的最小类型。除非某个示例额外声明了自己的 Data / Holder / Render,否则都可以直接与以下代码一起编译。
示例基础类型
共享类型代码
using AlicizaX.UI;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace RecyclerViewBookSamples
{
[System.Serializable]
public sealed class DemoSimpleData : ISimpleViewData
{
public string Title;
public string Subtitle;
public Sprite Icon;
}
[System.Serializable]
public sealed class DemoMixedData : IMixedViewData
{
public string TemplateName { get; set; } // 必须与模板名完全一致
public string Title;
public string Subtitle;
public Sprite Icon;
}
[System.Serializable]
public sealed class DemoGroupData : IGroupViewData
{
public string TemplateName { get; set; } // 组头与普通项共用此字段
public bool Expanded { get; set; } // 组头展开状态
public int Type { get; set; } // 分组键
public string Title;
}
public sealed class DemoItemHolder : ViewHolder
{
public Text title;
public Text subtitle;
public Image icon;
public Button actionButton;
public GameObject selectedMarker;
}
public sealed class DemoStateHolder : ViewHolder
{
public Text message;
public Button retryButton;
}
public sealed class DemoSimpleRender : ItemRender<DemoSimpleData, DemoItemHolder>
{
protected override void OnBind(DemoSimpleData data, int index)
{
Holder.title.text = $"{index + 1}. {data.Title}"; // 绑定标题
Holder.subtitle.text = data.Subtitle; // 绑定副标题
Holder.icon.sprite = data.Icon; // 绑定图标
}
protected override void OnSelectionChanged(bool selected)
{
if (Holder.selectedMarker != null)
{
Holder.selectedMarker.SetActive(selected); // 显示选中态
}
}
}
public sealed class DemoMixedRender : ItemRender<DemoMixedData, DemoItemHolder>
{
protected override void OnBind(DemoMixedData data, int index)
{
Holder.title.text = $"{index + 1}. {data.Title}"; // 绑定标题
Holder.subtitle.text = data.Subtitle; // 绑定副标题
Holder.icon.sprite = data.Icon; // 绑定图标
}
}
}
📌 所有示例都默认你已经在 Inspector 中把模板预制体上的
Text、Image、Button等引用拖好。
容器(RecyclerView)
RecyclerView 是系统的中心节点:它管理模板、内容区域、当前适配器、布局、滚动器、滚动条以及焦点导航。
API 说明
类型:RecyclerView
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/RecyclerView.cs
属性
| 成员名 | 类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
Direction |
Direction |
Direction.Vertical(未序列化时) |
主滚动方向。 | 与 LayoutManager、Scroller 的方向必须一致。 |
Alignment |
Alignment |
Alignment.Left(未序列化时) |
交叉轴对齐方式。 | Alignment.Center 会影响内容偏移计算。 |
Spacing |
Vector2 |
Vector2.zero |
Item 间距。 | 竖向列表通常使用 y;横向列表通常使用 x。 |
Padding |
Vector2 |
Vector2.zero |
内容内边距。 | 只影响布局计算,不会自动改模板尺寸。 |
Scroll |
bool |
false |
是否启用滚动。 | 关闭后 ScrollTo*() 直接失效。 |
Snap |
bool |
false |
是否在停下后吸附到最近项。 | 只有 Scroll == true 时才会真正生效。 |
ScrollSpeed |
float |
7f |
平滑滚动速度。 | 建议保持在 0.5f ~ 50f。 |
WheelSpeed |
float |
30f |
滚轮速度。 | 建议保持在 1f ~ 50f。 |
ShowScrollBarOnlyWhenScrollable |
bool |
false |
是否仅在内容溢出时显示滚动条。 | 仅对横向/纵向方向生效。 |
Templates |
ViewHolder[] |
null |
视图模板集合。 | 不能为空;多模板时模板名必须唯一。 |
Content |
RectTransform |
null,首次访问时尝试取第一个子节点 |
内容容器。 | 第一个子节点必须有 RectTransform。 |
Scrollbar |
Scrollbar |
null |
关联滚动条。 | 仅在启用滚动条时使用。 |
Scroller |
Scroller |
null |
当前滚动器实例。 | 通常由 Inspector 指定为 Scroller 或 CircleScroller。 |
ViewProvider |
ViewProvider |
懒加载 | 模板创建/回收提供器。 | 依赖 Templates 正常初始化。 |
PoolStats |
string |
string.Empty |
当前视图池统计文本。 | 仅用于诊断,格式不保证长期稳定。 |
LayoutManager |
LayoutManager |
null |
当前布局实例。 | 必须先配置,否则列表初始化时无法完成绑定。 |
NavigationController |
RecyclerNavigationController |
懒加载 | 列表导航控制器。 | 仅在导航场景下用到。 |
事件
| 事件名 | 类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
OnIndexChanged |
Action<int> |
null |
当前逻辑索引变化时触发。 | 对循环列表返回的是“真实索引”。 |
OnScrollValueChanged |
Action |
null |
滚动位置变化时触发。 | 高频事件,不要在回调里做重活。 |
💡 业务层如需感知当前焦点项,请监听
OnIndexChanged;如需读写业务选中态,请使用Adapter.ChoiceIndex。
方法
| 方法 | 返回值 | 默认参数 | 说明 | 使用限制 |
|---|---|---|---|---|
TryFocusIndex(int index, bool smooth = false, ScrollAlignment alignment = ScrollAlignment.Center) |
bool |
smooth=false, alignment=Center |
滚动并聚焦目标项。 | 目标索引必须在适配器合法范围内。 |
TryFocusEntry(MoveDirection entryDirection) |
bool |
无 | 从外部把焦点“送入”列表。 | 空列表返回 false。 |
GetScrollPosition() |
float |
无 | 获取当前滚动偏移。 | 没有滚动器时返回 0。 |
ScrollTo(int index, bool smooth = false) |
void |
smooth=false |
滚到指定项。 | 依赖 Scroll == true 且 Scroller != null。 |
ScrollToWithAlignment(int index, ScrollAlignment alignment, float offset = 0f, bool smooth = false, float duration = 0.3f) |
void |
offset=0f, smooth=false, duration=0.3f |
以指定对齐方式滚动到目标项。 | duration 仅在 smooth=true 时有意义。 |
参数:TryFocusIndex(int index, bool smooth = false, ScrollAlignment alignment = ScrollAlignment.Center)
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
index |
int |
无 | 目标数据索引。 |
smooth |
bool |
false |
是否先平滑滚动到可见区再聚焦。 |
alignment |
ScrollAlignment |
ScrollAlignment.Center |
聚焦时目标项的对齐方式。 |
- 返回值:
bool,成功定位并设置焦点时为true。 - 使用限制:空列表、越界索引或没有可聚焦对象都会返回
false。
参数:ScrollToWithAlignment(...)
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
index |
int |
无 | 目标索引。 |
alignment |
ScrollAlignment |
无 | 目标项停靠位置。 |
offset |
float |
0f |
在对齐结果基础上的附加偏移。 |
smooth |
bool |
false |
是否平滑滚动。 |
duration |
float |
0.3f |
平滑滚动时长。 |
- 返回值:无。
- 使用限制:
Scroll == false、Scroller == null或目标索引非法时无效果。
⚠️
ScrollTo()/TryFocusIndex()依赖列表已通过UGList.Data、Adapter.SetList()或Notify*()完成一次正式更新;布局与刷新会随这些入口自动完成,业务层不要额外手动补RequestLayout()/Refresh()。
初始化与刷新
示例 1:基础纵向列表初始化
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class BasicRecyclerViewDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: 绑定 LinearLayoutManager + Scroller
private UGList<DemoSimpleData> list;
private void Awake()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>(); // 注册默认模板渲染器
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "邮件", Subtitle = "今天 09:00" },
new DemoSimpleData { Title = "任务", Subtitle = "今天 10:30" },
new DemoSimpleData { Title = "公告", Subtitle = "今天 14:00" },
};
}
}
示例 2:异步拉取后再刷新容器
using System.Collections;
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class AsyncRecyclerViewDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: 模板与 LayoutManager 已配置
private UGList<DemoSimpleData> list;
private Adapter<DemoSimpleData> adapter;
private IEnumerator Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
adapter = list.Adapter;
yield return new WaitForSeconds(0.5f); // 模拟网络请求
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "远程数据 A", Subtitle = "加载完成" },
new DemoSimpleData { Title = "远程数据 B", Subtitle = "加载完成" },
}; // 赋值后框架自动布局并刷新
}
}
示例 3:空数据与安全刷新
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class EmptyStateRecyclerViewDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGList<DemoSimpleData> list;
private void OnEnable()
{
list ??= UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>(); // 空列表也是合法输入
bool focused = recyclerView.TryFocusIndex(0); // 空列表会返回 false
Debug.Log($"Focus result: {focused}");
}
}
定位与焦点控制
示例 1:按钮驱动滚动到指定项
using AlicizaX.UI;
using UnityEngine;
using UnityEngine.UI;
public sealed class ScrollToIndexDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
[SerializeField] private Button jumpButton;
private void Awake()
{
jumpButton.onClick.AddListener(() =>
{
recyclerView.ScrollToWithAlignment(
index: 10,
alignment: ScrollAlignment.Center,
offset: 0f,
smooth: true,
duration: 0.25f); // 以 0.25 秒平滑滚到中间
});
}
}
示例 2:打开面板时恢复焦点
using AlicizaX.UI;
using UnityEngine;
public sealed class FocusRecoveryDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
[SerializeField] private int lastSelectedIndex = 4;
private void OnEnable()
{
recyclerView.TryFocusIndex(lastSelectedIndex, true, ScrollAlignment.Center);
}
}
示例 3:监听焦点索引变化
using AlicizaX.UI;
using UnityEngine;
public sealed class FocusTraceDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private void Awake()
{
recyclerView.OnIndexChanged += index =>
Debug.Log($"Focused data index: {index}");
}
}
适配器(Adapter / GroupAdapter / LoopAdapter / MixedAdapter)
适配器负责把数据对象映射为模板名、绑定流程和局部刷新操作,是业务层最常直接扩展的模块。
API 说明
类型:Adapter<T>(where T : ISimpleViewData)
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/Adapter/Adapter.cs
构造与状态
| 成员名 | 类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
Adapter(RecyclerView recyclerView) |
构造函数 | list = new List<T>() |
创建空适配器。 | recyclerView 不能为空。 |
Adapter(RecyclerView recyclerView, List<T> list) |
构造函数 | list ?? new List<T>() |
创建带初始数据的适配器。 | 传入 null 会被替换为空列表。 |
ChoiceIndex |
int |
-1 |
当前选择索引。 | 超出范围会被自动收敛。 |
查询与绑定
| 方法 | 返回值 | 默认参数 | 说明 | 使用限制 |
|---|---|---|---|---|
GetItemCount() |
int |
无 | 当前布局项数量。 | 普通列表等于数据量;循环列表可能不是。 |
GetRealCount() |
int |
无 | 真实数据量。 | 循环列表场景优先用它。 |
GetViewName(int index) |
string |
无 | 返回模板名。 | 单模板默认返回空字符串。 |
OnBindViewHolder(ViewHolder viewHolder, int index) |
void |
无 | 绑定 Holder。 | 必须先注册与模板匹配的 ItemRender。 |
OnRecycleViewHolder(ViewHolder viewHolder) |
void |
无 | 回收前清理绑定。 | 业务层通常无需手动调用。 |
GetData(int index) |
T |
无 | 返回数据项。 | 越界返回 default。 |
刷新通知
| 方法 | 返回值 | 默认参数 | 说明 | 使用限制 |
|---|---|---|---|---|
NotifyDataChanged() |
void |
无 | 全量刷新。 | 代价最高。 |
SetList(List<T> list) |
void |
无 | 替换整个数据源。 | 会触发 RecyclerView.Reset()。 |
NotifyItemChanged(int index, bool relayout = false) |
void |
relayout=false |
刷新单项。 | 仅当项尺寸不变时推荐 relayout=false。 |
NotifyItemRangeChanged(int index, int count, bool relayout = false) |
void |
relayout=false |
刷新区间。 | count <= 0 直接返回。 |
NotifyItemInserted(int index) |
void |
无 | 通知插入。 | 当前实现会重新请求布局并刷新。 |
NotifyItemRangeInserted(int index, int count) |
void |
无 | 通知批量插入。 | count <= 0 直接返回。 |
NotifyItemRemoved(int index) |
void |
无 | 通知删除。 | 当前实现会重新请求布局并刷新。 |
NotifyItemRangeRemoved(int index, int count) |
void |
无 | 通知批量删除。 | count <= 0 直接返回。 |
渲染器注册
| 方法 | 返回值 | 默认参数 | 说明 | 使用限制 |
|---|---|---|---|---|
RegisterItemRender<TItemRender>(string viewName = "") |
void |
viewName="" |
以泛型注册渲染器。 | 空字符串表示默认模板。 |
RegisterItemRender(Type itemRenderType, string viewName = "") |
void |
viewName="" |
以运行时类型注册渲染器。 | 类型必须继承 ItemRenderBase。 |
UnregisterItemRender(string viewName = "") |
bool |
viewName="" |
注销渲染器。 | 注销后相关缓存会被释放。 |
ClearItemRenderRegistrations() |
void |
无 | 清空所有注册。 | 适合切换整套模板时使用。 |
集合操作
| 方法 | 返回值 | 默认参数 | 说明 | 使用限制 |
|---|---|---|---|---|
Add(T item) |
void |
无 | 末尾追加。 | 自动触发插入通知。 |
AddRange(IEnumerable<T> collection) |
void |
无 | 批量追加。 | collection == null 时忽略。 |
Insert(int index, T item) |
void |
无 | 指定位置插入。 | 越界由 List<T> 自己抛错。 |
InsertRange(int index, IEnumerable<T> collection) |
void |
无 | 批量插入。 | collection == null 时忽略。 |
Remove(T item) |
void |
无 | 删除首个匹配项。 | 找不到时等价于 RemoveAt(-1),最终无效果。 |
RemoveAt(int index) |
void |
无 | 删除指定索引。 | 越界直接返回。 |
RemoveRange(int index, int count) |
void |
无 | 删除区间。 | 参数必须满足 List<T>.RemoveRange 要求。 |
RemoveAll(Predicate<T> match) |
void |
无 | 条件删除。 | 始终触发全量刷新。 |
Clear() |
void |
无 | 清空列表。 | 空列表时直接返回。 |
Reverse() / Reverse(int index, int count) |
void |
无 | 反转顺序。 | 最终触发全量刷新。 |
Sort(Comparison<T> comparison) |
void |
无 | 排序。 | 最终触发全量刷新。 |
类型:GroupAdapter<TData>(where TData : IGroupViewData, new())
| 方法/成员 | 返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
GroupAdapter(RecyclerView recyclerView, string groupViewName) |
构造函数 | 无 | 指定组头模板名。 | NotifyDataChanged() 前必须传非空模板名。 |
Expand(int index) |
void |
无 | 展开组头后的子项。 | index 必须指向组头。 |
Collapse(int index) |
void |
无 | 收起某组。 | 不会改原始 list,只改展示列表。 |
Activate(int index) |
void |
无 | 组头时切换展开;普通项时切换选中。 | 常用于点击事件。 |
类型:LoopAdapter<T>
| 方法/成员 | 返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
GetItemCount() |
int |
无 | 有数据时返回 int.MaxValue。 |
只适合循环/轮播场景。 |
GetRealCount() |
int |
无 | 返回真实数据量。 | 业务逻辑应优先使用此值。 |
OnBindViewHolder(...) |
void |
无 | 绑定时对索引做取模。 | 数据为空时不绑定。 |
类型:MixedAdapter<TData>(where TData : IMixedViewData)
| 方法/成员 | 返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
GetViewName(int index) |
string |
无 | 返回 TemplateName。 |
模板名必须与 Templates[].name 一致。 |
参数:RegisterItemRender(Type itemRenderType, string viewName = "")
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
itemRenderType |
Type |
无 | 渲染器运行时类型。 |
viewName |
string |
"" |
关联模板名;空字符串表示默认模板。 |
- 返回值:无。
- 使用限制:类型必须可实例化,且继承自
ItemRenderBase。
参数:NotifyItemChanged(int index, bool relayout = false)
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
index |
int |
无 | 目标数据索引。 |
relayout |
bool |
false |
是否重新计算布局。 |
- 返回值:无。
- 使用限制:如果文本高度、图片尺寸、模板类型发生变化,应该传
relayout=true。
基础数据更新
示例 1:通过 UGList.Adapter 进行增删改
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class AdapterCrudDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGList<DemoSimpleData> list;
private Adapter<DemoSimpleData> adapter;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>();
adapter = list.Adapter;
adapter.Add(new DemoSimpleData { Title = "第一条", Subtitle = "Add()" }); // 自动触发插入
adapter.Add(new DemoSimpleData { Title = "第二条", Subtitle = "Add()" });
adapter.Insert(1, new DemoSimpleData { Title = "插入项", Subtitle = "Insert()" });
}
}
示例 2:差量刷新单项与区间
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class AdapterPartialRefreshDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGList<DemoSimpleData> list;
private Adapter<DemoSimpleData> adapter;
private readonly List<DemoSimpleData> data = new();
private void Start()
{
data.Add(new DemoSimpleData { Title = "HP", Subtitle = "100" });
data.Add(new DemoSimpleData { Title = "MP", Subtitle = "60" });
data.Add(new DemoSimpleData { Title = "SP", Subtitle = "30" });
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = data;
adapter = list.Adapter;
}
public void UpdateHud()
{
data[0].Subtitle = "95";
adapter.NotifyItemChanged(0); // 尺寸不变,只重绑可见项
data[1].Subtitle = "58";
data[2].Subtitle = "28";
adapter.NotifyItemRangeChanged(1, 2); // 批量局部刷新
}
}
示例 3:尺寸变化时强制重布局
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class AdapterRelayoutDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGList<DemoSimpleData> list;
private Adapter<DemoSimpleData> adapter;
private readonly List<DemoSimpleData> data = new();
private void Start()
{
data.Add(new DemoSimpleData { Title = "短文本", Subtitle = "1 行" });
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = data;
adapter = list.Adapter;
}
public void ExpandText()
{
data[0].Subtitle = "这一条文本被拉长后可能导致高度变化,因此需要 relayout=true。";
adapter.NotifyItemChanged(0, relayout: true); // 尺寸变化时必须重布局
}
}
多模板、分组与循环
示例 1:多模板注册与绑定
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class DemoLargeHolder : ViewHolder
{
public UnityEngine.UI.Text title;
public UnityEngine.UI.Text subtitle;
}
public sealed class DemoLargeRender : ItemRender<DemoMixedData, DemoLargeHolder>
{
protected override void OnBind(DemoMixedData data, int index)
{
Holder.title.text = $"[L] {data.Title}"; // 大卡片模板
Holder.subtitle.text = data.Subtitle;
}
}
public sealed class MixedAdapterDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: Templates 含 DemoItemHolder 与 DemoLargeHolder
private UGMixedList<DemoMixedData> list;
private void Start()
{
list = UGListCreateHelper.CreateMixed<DemoMixedData>(recyclerView);
list.RegisterItemRender<DemoMixedRender>("SmallItem"); // 绑定小模板
list.RegisterItemRender<DemoLargeRender>("LargeItem"); // 绑定大模板
list.Data = new List<DemoMixedData>
{
new DemoMixedData { TemplateName = "LargeItem", Title = "头图", Subtitle = "大卡片" },
new DemoMixedData { TemplateName = "SmallItem", Title = "正文 A", Subtitle = "小卡片" },
new DemoMixedData { TemplateName = "SmallItem", Title = "正文 B", Subtitle = "小卡片" },
};
}
}
示例 2:分组展开/收起
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class GroupHeaderRender : ItemRender<DemoGroupData, DemoItemHolder>
{
protected override void OnBind(DemoGroupData data, int index)
{
Holder.title.text = data.Expanded ? $"▼ {data.Title}" : $"▶ {data.Title}";
Holder.subtitle.text = $"Type = {data.Type}";
}
}
public sealed class GroupItemRender : ItemRender<DemoGroupData, DemoItemHolder>
{
protected override void OnBind(DemoGroupData data, int index)
{
Holder.title.text = data.Title;
Holder.subtitle.text = $"Group {data.Type}";
}
}
public sealed class GroupAdapterDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGGroupList<DemoGroupData> list;
private void Start()
{
list = UGListCreateHelper.CreateGroup<DemoGroupData>(recyclerView, "GroupHeader");
list.RegisterItemRender<GroupHeaderRender>("GroupHeader");
list.RegisterItemRender<GroupItemRender>("GroupItem");
list.Data = new List<DemoGroupData>
{
new DemoGroupData { TemplateName = "GroupItem", Type = 1, Title = "剑" },
new DemoGroupData { TemplateName = "GroupItem", Type = 1, Title = "盾" },
new DemoGroupData { TemplateName = "GroupItem", Type = 2, Title = "药水" },
};
}
public void ToggleFirstGroup()
{
list.Adapter.Activate(0); // 组头位置会切换 Expanded
}
}
示例 3:循环适配器的虚拟索引
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class LoopAdapterDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGLoopList<DemoSimpleData> list;
private int virtualIndex;
private void Start()
{
list = UGListCreateHelper.CreateLoop<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "Banner A" },
new DemoSimpleData { Title = "Banner B" },
new DemoSimpleData { Title = "Banner C" },
};
virtualIndex = list.Adapter.GetRealCount() * 100; // 从中间起步,便于双向循环
recyclerView.ScrollToWithAlignment(virtualIndex, ScrollAlignment.Center);
}
public void MoveNext()
{
virtualIndex += 1; // 维护虚拟索引,而不是只用真实索引
recyclerView.ScrollToWithAlignment(virtualIndex, ScrollAlignment.Center, 0f, true, 0.2f);
}
}
布局(LayoutManager 系列)
布局器只负责“算位置”,不负责数据绑定。它决定内容尺寸、可见区间、索引到坐标的映射以及吸附基准。
API 说明
类型:LayoutManager
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/Layout/LayoutManager.cs
核心属性
| 成员名 | 类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
ViewportSize |
Vector2 |
运行时计算 | 视口尺寸。 | 只有执行过 SetContentSize() 后才可靠。 |
ContentSize |
Vector2 |
运行时计算 | 内容总尺寸。 | 滚动条与滚动范围都依赖此值。 |
ContentOffset |
Vector2 |
运行时计算 | 内容对齐偏移。 | 与 Alignment 有关。 |
ViewportOffset |
Vector2 |
运行时计算 | 视口偏移。 | 分页/居中场景常用。 |
Adapter |
IAdapter |
null |
当前适配器。 | 由容器内部绑定流程注入。 |
ViewProvider |
ViewProvider |
null |
当前视图提供器。 | 由容器内部绑定流程注入。 |
RecyclerView |
RecyclerView |
null |
所属列表。 | 由容器内部绑定流程注入。 |
Direction |
Direction |
Direction.Vertical |
主轴方向。 | 应与容器一致。 |
Alignment |
Alignment |
Alignment.Left |
对齐方式。 | Center 会改变偏移计算。 |
Spacing |
Vector2 |
Vector2.zero |
间距。 | 按方向使用对应轴。 |
Padding |
Vector2 |
Vector2.zero |
内边距。 | 直接参与内容尺寸计算。 |
Unit |
int |
1 |
每次创建/回收的步进量。 | 网格通常等于一行或一列的元素数。 |
ScrollPosition |
float |
运行时读取 | 当前滚动位置。 | 只读,来自 RecyclerView.GetScrollPosition()。 |
通用方法
| 方法 | 返回值 | 默认参数 | 说明 | 使用限制 |
|---|---|---|---|---|
SetContentSize() |
void |
无 | 计算视口、内容、偏移。 | 由容器内部布局流程调用。 |
UpdateLayout() |
void |
无 | 重新布局当前可见 Holder。 | 仅操作已创建视图。 |
Layout(ViewHolder viewHolder, int index) |
void |
无 | 将指定 Holder 摆放到目标位置。 | 默认按 CalculatePosition() 放置。 |
CalculateContentSize() |
Vector2 |
无 | 计算内容总尺寸。 | 由具体布局实现。 |
CalculatePosition(int index) |
Vector2 |
无 | 计算指定索引位置。 | 由具体布局实现。 |
CalculateContentOffset() |
Vector2 |
无 | 计算内容偏移。 | 与对齐方式强相关。 |
CalculateViewportOffset() |
Vector2 |
无 | 计算视口偏移。 | 分页与居中布局更明显。 |
GetStartIndex() / GetEndIndex() |
int |
无 | 当前滚动位置的可见区间。 | end < start 时表示当前无需创建项。 |
IndexToPosition(int index) |
float |
无 | 索引转滚动位置。 | 吸附与 ScrollTo 都依赖它。 |
PositionToIndex(float position) |
int |
无 | 滚动位置转最近索引。 | 吸附时用来找最近项。 |
DoItemAnimation() |
void |
无 | 执行布局相关动画。 | PageLayoutManager、CircleLayoutManager 会覆写。 |
类型:布局派生类
| 类型 | 主要特性 | 默认值 | 使用限制 |
|---|---|---|---|
LinearLayoutManager |
等尺寸线性布局。 | 无额外字段 | 适合单列/单行列表。 |
GridLayoutManager |
网格布局。 | cellCount = 1 |
cellCount 通过 Inspector 配置。 |
PageLayoutManager |
分页式线性布局,带缩放动画。 | minScale = 0.9f |
常与 Snap 联用。 |
CircleLayoutManager |
圆环布局。 | circleDirection = Positive |
intervalAngle 在运行时按项数重算。 |
MixedLayoutManager |
支持不同模板尺寸的异构布局。 | 内部缓存为空 | 最适合多模板或变高度列表。 |
参数:IndexToPosition(int index)
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
index |
int |
无 | 目标布局索引。 |
- 返回值:
float,滚动器目标位置。 - 使用限制:越界时通常被实现类收敛到合法范围,但业务层仍应传递有效索引。
线性与网格布局
示例 1:线性列表
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class LinearLayoutDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: LayoutManager = LinearLayoutManager
private UGList<DemoSimpleData> list;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "线性列表 1" },
new DemoSimpleData { Title = "线性列表 2" },
new DemoSimpleData { Title = "线性列表 3" },
};
}
}
示例 2:网格背包
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class GridInventoryDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: LayoutManager = GridLayoutManager, cellCount = 4
private UGList<DemoSimpleData> list;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
var items = new List<DemoSimpleData>();
for (int i = 0; i < 20; i++)
{
items.Add(new DemoSimpleData { Title = $"格子 {i + 1}", Subtitle = $"Index={i}" });
}
list.Data = items;
}
}
示例 3:空网格的边界结果
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class EmptyGridDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private void Start()
{
var list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>(); // 空网格
}
}
分页、圆环与异构长度布局
示例 1:分页卡片
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class PageLayoutDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: LayoutManager = PageLayoutManager, Snap = true
private UGList<DemoSimpleData> list;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "Page 1" },
new DemoSimpleData { Title = "Page 2" },
new DemoSimpleData { Title = "Page 3" },
};
}
}
示例 2:圆环菜单
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class CircleMenuDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: LayoutManager = CircleLayoutManager, Scroller = CircleScroller
private UGList<DemoSimpleData> list;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "装备" },
new DemoSimpleData { Title = "技能" },
new DemoSimpleData { Title = "任务" },
new DemoSimpleData { Title = "地图" },
};
}
}
示例 3:异构长度列表
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class MixedLayoutDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: LayoutManager = MixedLayoutManager
private UGMixedList<DemoMixedData> list;
private void Start()
{
list = UGListCreateHelper.CreateMixed<DemoMixedData>(recyclerView);
list.RegisterItemRender<DemoMixedRender>("SmallItem");
list.RegisterItemRender<DemoMixedRender>("LargeItem"); // 两个模板尺寸不同
list.Data = new List<DemoMixedData>
{
new DemoMixedData { TemplateName = "LargeItem", Title = "长卡片" },
new DemoMixedData { TemplateName = "SmallItem", Title = "短卡片 A" },
new DemoMixedData { TemplateName = "SmallItem", Title = "短卡片 B" },
};
}
}
滚动器(Scroller / CircleScroller)
滚动器负责手势输入与位移变化;布局负责“每个位移意味着什么”。二者解耦后,滚动和显示方式可以灵活组合。
API 说明
类型:Scroller
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/Scroller/Scroller.cs
属性
| 成员名 | 类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
Position |
float |
0f |
当前滚动位置。 | 直接赋值不会自动刷新 UI,通常由 RecyclerView 驱动。 |
Velocity |
float |
只读 | 当前速度。 | 仅用于诊断或自定义效果。 |
Direction |
Direction |
Direction.Vertical |
滚动方向。 | 应与容器方向一致。 |
ContentSize |
Vector2 |
Vector2.zero |
内容尺寸。 | 由容器内部布局流程同步更新。 |
ViewSize |
Vector2 |
Vector2.zero |
视口尺寸。 | 由容器内部布局流程同步更新。 |
ScrollSpeed |
float |
1f |
MoveTo 速度。 |
外部通常通过 RecyclerView.ScrollSpeed 配置。 |
WheelSpeed |
float |
30f |
滚轮速度。 | 过大容易导致过冲。 |
Snap |
bool |
false |
是否启用吸附模式。 | 吸附由容器在停止时完成。 |
MaxPosition |
float |
只读 | 最大合法滚动位置。 | 由内容和视口尺寸计算。 |
ViewLength |
float |
只读 | 视口主轴长度。 | 用于边缘阻尼。 |
OnValueChanged |
ScrollerEvent |
新实例 | 位移变化事件。 | 高频事件。 |
OnMoveStoped |
MoveStopEvent |
新实例 | 惯性/动画停止事件。 | 拼写保留源码 Stoped。 |
OnDragging |
DraggingEvent |
新实例 | 拖拽开始/结束事件。 | 参数为是否正在拖拽。 |
dragStopTime |
float |
0f |
最近一次拖拽时间戳。 | 通常仅供调试。 |
方法
| 方法 | 返回值 | 默认参数 | 说明 | 使用限制 |
|---|---|---|---|---|
ScrollTo(float position, bool smooth = false) |
void |
smooth=false |
滚到目标位置。 | 不主动夹取范围,推荐经由 RecyclerView 调用。 |
ScrollToDuration(float position, float duration) |
void |
无 | 按固定时长滚动。 | duration <= 0 时立即到位。 |
ScrollToRatio(float ratio) |
void |
无 | 以归一化滚动条值滚动。 | 通常由滚动条驱动。 |
OnBeginDrag(...) / OnDrag(...) / OnEndDrag(...) |
void |
无 | 处理拖拽输入。 | 通常由 Unity 事件系统自动调用。 |
OnScroll(PointerEventData eventData) |
void |
无 | 处理鼠标滚轮。 | 横向列表读取 scrollDelta.x。 |
类型:CircleScroller
| 方法/成员 | 返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
GetDelta(PointerEventData eventData) |
float |
无 | 根据环形中心计算拖拽方向。 | 仅适合圆环布局。 |
Elastic() |
void |
无 | 被覆写为空实现。 | 圆环一般不需要边界回弹。 |
参数:ScrollTo(float position, bool smooth = false)
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
position |
float |
无 | 目标滚动位置。 |
smooth |
bool |
false |
是否使用平滑补间。 |
- 返回值:无。
- 使用限制:该方法不会主动把
position夹到0 ~ MaxPosition,需要上层自行保证。
程序化滚动
示例 1:立即滚动与平滑滚动
using AlicizaX.UI;
using UnityEngine;
public sealed class ProgrammaticScrollDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
public void JumpToTop()
{
recyclerView.ScrollTo(0, smooth: false); // 立即跳到顶部
}
public void SmoothToMiddle()
{
recyclerView.ScrollToWithAlignment(15, ScrollAlignment.Center, 0f, true, 0.35f);
}
}
示例 2:按固定时长滚动
using AlicizaX.UI;
using UnityEngine;
public sealed class TimedScrollDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
public void ScrollByDuration()
{
if (recyclerView.Scroller == null)
{
return;
}
float target = Mathf.Min(recyclerView.Scroller.MaxPosition, 320f);
recyclerView.Scroller.ScrollToDuration(target, 0.5f); // 0.5 秒到位
}
}
示例 3:无滚动能力时的防御式调用
using AlicizaX.UI;
using UnityEngine;
public sealed class ScrollGuardDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
public void TryScroll()
{
if (!recyclerView.Scroll || recyclerView.Scroller == null)
{
Debug.Log("Scroll disabled, ignore request."); // 避免空滚动器导致误判
return;
}
recyclerView.ScrollToWithAlignment(2, ScrollAlignment.Start);
}
}
惯性、吸附与自动播放
示例 1:分页列表的吸附体验
using AlicizaX.UI;
using UnityEngine;
public sealed class SnapPageDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: Snap = true, LayoutManager = PageLayoutManager
private void Awake()
{
recyclerView.Scroll = true; // 启用滚动
recyclerView.Snap = true; // 停止时自动吸附最近项
recyclerView.ScrollSpeed = 10f; // 提高分页吸附速度
}
}
示例 2:自动播放轮播
using System.Collections;
using AlicizaX.UI;
using UnityEngine;
public sealed class AutoPlayDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private int nextIndex;
private Coroutine autoPlayCoroutine;
private void OnEnable()
{
autoPlayCoroutine = StartCoroutine(AutoPlay());
}
private void OnDisable()
{
if (autoPlayCoroutine != null)
{
StopCoroutine(autoPlayCoroutine); // 面板关闭时停止自动滚动
}
}
private IEnumerator AutoPlay()
{
while (true)
{
yield return new WaitForSeconds(3f);
recyclerView.ScrollToWithAlignment(nextIndex, ScrollAlignment.Center, 0f, true, 0.25f);
nextIndex += 1; // 循环列表可持续递增
}
}
}
示例 3:手动上一页 / 下一页
using AlicizaX.UI;
using UnityEngine;
using UnityEngine.UI;
public sealed class PagerButtonsDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
[SerializeField] private Button prevButton;
[SerializeField] private Button nextButton;
private int currentIndex;
private void Awake()
{
recyclerView.OnIndexChanged += index => currentIndex = Mathf.Max(index, 0);
prevButton.onClick.AddListener(() =>
{
recyclerView.ScrollToWithAlignment(Mathf.Max(currentIndex - 1, 0), ScrollAlignment.Center, 0f, true, 0.2f);
});
nextButton.onClick.AddListener(() =>
{
recyclerView.ScrollToWithAlignment(currentIndex + 1, ScrollAlignment.Center, 0f, true, 0.2f);
});
}
}
视图池(ViewProvider / ObjectPool)
视图池是 RecyclerView 性能表现的核心:它让“大列表”只保留可见区对象,其他对象进入可复用池而不是销毁重建。
API 说明
类型:ViewProvider
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/ViewProvider/ViewProvider.cs
| 方法/成员 | 返回值/类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
Adapter |
IAdapter |
null |
当前适配器。 | 由容器注入。 |
LayoutManager |
LayoutManager |
null |
当前布局器。 | 由容器注入。 |
ViewHolders |
IReadOnlyList<ViewHolder> |
空集合 | 当前可见 Holder 集。 | 只读视图。 |
PoolStats |
string |
抽象成员 | 对象池统计。 | 用于诊断。 |
GetTemplate(string viewName) |
ViewHolder |
无 | 获取模板。 | 多模板时名字必须命中。 |
GetTemplates() |
ViewHolder[] |
无 | 获取所有模板。 | 模板不能为空。 |
Allocate(string viewName) |
ViewHolder |
无 | 从池中取出一个 Holder。 | 通常不手动调用。 |
Free(string viewName, ViewHolder viewHolder) |
void |
无 | 归还 Holder。 | 通常不手动调用。 |
Reset() |
void |
无 | 清空可见项并销毁池。 | 常用于彻底重建。 |
PreparePool() |
void |
无 | 依据可见区预热池。 | 由容器内部布局流程触发。 |
CreateViewHolder(int index) |
void |
无 | 创建一个 Unit 批次的 Holder。 |
index 为布局起点。 |
RemoveViewHolder(int index) |
void |
无 | 回收一个 Unit 批次的 Holder。 |
index 为布局起点。 |
GetViewHolder(int index) |
ViewHolder |
无 | 根据布局索引查找 Holder。 | 不可见时返回 null。 |
GetViewHolderByDataIndex(int dataIndex) |
ViewHolder |
无 | 根据数据索引查找 Holder。 | 循环列表可能存在多个映射。 |
TryGetViewHoldersByDataIndex(...) |
bool |
无 | 获取某数据索引对应的所有 Holder。 | 适合循环列表。 |
CalculateViewSize(int index) |
Vector2 |
无 | 按模板估算尺寸。 | 依赖 GetViewName(index) 正确。 |
类型:SimpleViewProvider
| 成员 | 类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
| 内部池 | ObjectPool<ViewHolder> |
initialSize = 0, maxSize = 1 |
单模板池。 | 实际容量会在 PreparePool() 中扩容。 |
PoolStats |
string |
实时统计 | 命中、未命中、销毁、活跃等信息。 | 文本格式仅用于日志。 |
类型:MixedViewProvider
| 成员 | 类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
| 内部池 | MixedObjectPool<ViewHolder> |
每类型默认上限 10 |
多模板池。 | 模板名必须唯一。 |
PoolStats |
string |
实时统计 | 当前命中、未命中、销毁信息。 | 细粒度活跃数需看 MixedObjectPool。 |
类型:ObjectPool<T>
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/ObjectPool/ObjectPool.cs
| 方法/成员 | 返回值/类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
ObjectPool(IObjectFactory<T> factory) |
构造函数 | maxSize = Environment.ProcessorCount * 2 |
用 CPU 数量推导上限。 | 初始大小为 0。 |
ObjectPool(IObjectFactory<T> factory, int maxSize) |
构造函数 | initialSize = 0 |
指定最大池容量。 | maxSize 需大于等于 0。 |
ObjectPool(IObjectFactory<T> factory, int initialSize, int maxSize) |
构造函数 | 无 | 指定预热数量与上限。 | maxSize < initialSize 会抛异常。 |
Allocate() |
T |
无 | 取对象。 | 空池时会创建新对象。 |
Free(T obj) |
void |
无 | 归还对象。 | 超过上限时会销毁。 |
EnsureCapacity(int value) |
void |
无 | 调大池上限。 | value <= 0 抛异常。 |
Warm(int count) |
void |
无 | 预热到目标数量。 | 实际不会超过 maxSize。 |
Dispose() |
void |
无 | 销毁所有空闲对象。 | 活跃对象不受影响。 |
MaxSize |
int |
构造决定 | 当前最大容量。 | 可被 EnsureCapacity() 提高。 |
ActiveCount / InactiveCount |
int |
0 |
活跃 / 闲置数量。 | 用于评估是否需要调容量。 |
HitCount / MissCount / DestroyCount |
int |
0 |
命中、未命中、销毁统计。 | 用于调优。 |
PeakActive |
int |
0 |
历史峰值活跃数。 | 可作为容量基线。 |
类型:MixedObjectPool<T>
| 方法/成员 | 返回值/类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
MixedObjectPool(IMixedObjectFactory<T> factory) |
构造函数 | defaultMaxSizePerType = 10 |
使用默认每类型容量。 | factory 不能为空。 |
MixedObjectPool(IMixedObjectFactory<T> factory, int defaultMaxSizePerType) |
构造函数 | 无 | 显式指定每类型默认容量。 | defaultMaxSizePerType <= 0 抛异常。 |
Allocate(string typeName) |
T |
无 | 取指定模板类型对象。 | typeName 必须存在。 |
Free(string typeName, T obj) |
void |
无 | 归还指定类型对象。 | 超出该类型上限时会销毁。 |
SetMaxSize(string typeName, int value) |
void |
无 | 设置单类型上限。 | 不会自动创建对象。 |
EnsureCapacity(string typeName, int value) |
void |
无 | 单类型扩容。 | value <= 0 抛异常。 |
Warm(string typeName, int count) |
void |
无 | 单类型预热。 | 实际不会超过该类型上限。 |
GetActiveCount(string typeName) |
int |
0 |
当前活跃数。 | 适合按模板调优。 |
GetPeakActiveCount(string typeName) |
int |
0 |
历史峰值活跃数。 | 可用于回写容量。 |
预热与复用
示例 1:依赖内建 PreparePool() 自动预热
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class BuiltinPoolWarmDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private void Start()
{
var list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
var data = new List<DemoSimpleData>();
for (int i = 0; i < 200; i++)
{
data.Add(new DemoSimpleData { Title = $"行 {i + 1}" });
}
list.Data = data;
Debug.Log(recyclerView.PoolStats); // 输出当前池统计
}
}
示例 2:直接使用 ObjectPool<T> 自定义容量
using AlicizaX.UI;
using UnityEngine;
using UnityEngine.UI;
public sealed class DemoPoolHolder : ViewHolder
{
public Text title;
}
public sealed class DemoPoolFactory : IObjectFactory<DemoPoolHolder>
{
private readonly DemoPoolHolder template;
private readonly Transform parent;
public DemoPoolFactory(DemoPoolHolder template, Transform parent)
{
this.template = template;
this.parent = parent;
}
public DemoPoolHolder Create()
{
return Object.Instantiate(template, parent); // 池空时创建
}
public void Reset(DemoPoolHolder obj)
{
obj.gameObject.SetActive(false); // 归还时隐藏
}
public bool Validate(DemoPoolHolder obj)
{
return obj != null; // 被销毁对象不再回池
}
public void Destroy(DemoPoolHolder obj)
{
Object.Destroy(obj.gameObject); // 超上限时销毁
}
}
public sealed class ObjectPoolDemo : MonoBehaviour
{
[SerializeField] private DemoPoolHolder template;
[SerializeField] private Transform parent;
private ObjectPool<DemoPoolHolder> pool;
private void Awake()
{
pool = new ObjectPool<DemoPoolHolder>(new DemoPoolFactory(template, parent), initialSize: 8, maxSize: 32);
pool.Warm(16); // 预热到 16 个
pool.EnsureCapacity(48); // 动态扩容到 48
}
private void Start()
{
DemoPoolHolder holder = pool.Allocate();
holder.title.text = "From pool";
pool.Free(holder); // 归还后进入闲置栈
Debug.Log($"hit={pool.HitCount}, miss={pool.MissCount}, inactive={pool.InactiveCount}");
}
}
示例 3:校验失败时自动销毁
using AlicizaX.UI;
using UnityEngine;
public sealed class PoolValidateDemo : MonoBehaviour
{
[SerializeField] private DemoPoolHolder template;
[SerializeField] private Transform parent;
private ObjectPool<DemoPoolHolder> pool;
private void Start()
{
pool = new ObjectPool<DemoPoolHolder>(new DemoPoolFactory(template, parent), 1, 2);
DemoPoolHolder holder = pool.Allocate();
Destroy(holder.gameObject); // 模拟外部错误销毁
pool.Free(holder); // Validate 失败后不会重新入池
}
}
多模板对象池配置
示例 1:按模板类型分别预热
using System.Collections.Generic;
using AlicizaX.UI;
using UnityEngine;
public sealed class MixedPoolDemo : MonoBehaviour
{
[SerializeField] private DemoPoolHolder smallTemplate;
[SerializeField] private DemoPoolHolder largeTemplate;
[SerializeField] private Transform parent;
private MixedObjectPool<DemoPoolHolder> pool;
private void Awake()
{
var templates = new Dictionary<string, DemoPoolHolder>
{
["SmallItem"] = smallTemplate,
["LargeItem"] = largeTemplate,
};
pool = new MixedObjectPool<DemoPoolHolder>(
new UnityMixedComponentFactory<DemoPoolHolder>(templates, parent),
defaultMaxSizePerType: 6);
pool.EnsureCapacity("SmallItem", 24); // 小卡片出现频率更高
pool.EnsureCapacity("LargeItem", 8); // 大卡片数量更少
pool.Warm("SmallItem", 16);
pool.Warm("LargeItem", 4);
}
}
示例 2:根据峰值活跃数回写容量
using AlicizaX.UI;
using UnityEngine;
public sealed class MixedPoolTuningDemo : MonoBehaviour
{
public void PrintRecommendation(MixedObjectPool<DemoPoolHolder> pool)
{
int peak = pool.GetPeakActiveCount("SmallItem");
int recommended = peak + 2; // 峰值 + buffer
Debug.Log($"Recommended SmallItem capacity = {recommended}");
}
}
导航(RecyclerNavigation)
导航模块负责把 EventSystem 的移动事件转成“列表内跳转 + 必要滚动 + 焦点恢复”。
API 说明
类型:RecyclerNavigationController
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/Navigation/RecyclerNavigationController.cs
| 方法/成员 | 返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
RecyclerNavigationController(RecyclerView recyclerView) |
构造函数 | 无 | 创建导航控制器。 | 一般由 RecyclerView.NavigationController 懒加载。 |
TryMove(ViewHolder currentHolder, MoveDirection direction, RecyclerNavigationOptions options) |
bool |
无 | 尝试从当前项移动到下一个项。 | currentHolder 不能为空,空列表返回 false。 |
类型:RecyclerNavigationBridge
| 成员/方法 | 类型/返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
defaultEntryDirection |
MoveDirection |
MoveDirection.Down |
列表首次接收焦点时的进入方向。 | Inspector 配置。 |
OnSelect(...) |
void |
无 | 当桥节点被选中时尝试进入列表。 | 挂在 RecyclerView 同节点上。 |
OnMove(...) |
void |
无 | 当桥节点收到方向键时尝试进入列表。 | 失败后才回落到原 Selectable 行为。 |
OnSubmit(...) |
void |
无 | 提交键进入列表。 | 常用于确认键。 |
类型:RecyclerNavigationOptions
| 成员 | 类型 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
Wrap |
bool |
构造决定 | 是否允许首尾环绕。 | 对非循环列表通常应关闭。 |
SmoothScroll |
bool |
构造决定 | 导航时是否平滑滚动。 | 开启后焦点会等待滚动完成。 |
Alignment |
ScrollAlignment |
构造决定 | 导航触发滚动时的对齐方式。 | 常用 Center。 |
Clamped |
RecyclerNavigationOptions |
wrap=false, smooth=false, alignment=Center |
不环绕方案。 | 静态只读。 |
Circular |
RecyclerNavigationOptions |
wrap=true, smooth=false, alignment=Center |
允许环绕方案。 | 静态只读。 |
类型:ItemInteractionProxy / RecyclerItemSelectable
| 成员/方法 | 类型/返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
GetSelectable() |
Selectable |
无 | 返回真正参与导航的焦点锚点。 | 若节点上没有 Selectable 会自动补 RecyclerItemSelectable。 |
Bind(IItemInteractionHost) |
void |
无 | 绑定交互宿主。 | 由 ItemRender 内部驱动。 |
Clear() |
void |
无 | 清理交互宿主与状态。 | 回收时自动调用。 |
列表内导航
示例 1:ItemRender 启用点击、移动与提交
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
using UnityEngine.EventSystems;
public sealed class NavigableItemRender : ItemRender<DemoSimpleData, DemoItemHolder>
{
public override ItemInteractionFlags InteractionFlags =>
ItemInteractionFlags.PointerClick |
ItemInteractionFlags.Select |
ItemInteractionFlags.Move |
ItemInteractionFlags.Submit;
protected override void OnBind(DemoSimpleData data, int index)
{
Holder.title.text = data.Title;
Holder.subtitle.text = $"可导航项 {index}";
}
protected override void OnPointerClick(PointerEventData eventData)
{
Debug.Log($"Clicked {CurrentIndex}"); // 点击时选择当前项
}
protected override void OnSubmit(BaseEventData eventData)
{
Debug.Log($"Submit {CurrentIndex}"); // 回车 / A 键确认
}
}
示例 2:主动驱动导航器移动
using AlicizaX.UI;
using UnityEngine;
public sealed class ManualNavigationDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private int currentIndex;
private void Awake()
{
recyclerView.OnIndexChanged += index => currentIndex = Mathf.Max(index, 0);
}
private void Update()
{
if (!Input.GetKeyDown(KeyCode.RightArrow))
{
return;
}
var current = EventSystem.current?.currentSelectedGameObject?.GetComponentInParent<ViewHolder>();
if (current == null)
{
recyclerView.TryFocusIndex(currentIndex, false, ScrollAlignment.Center);
return;
}
recyclerView.NavigationController.TryMove(current, MoveDirection.Right, RecyclerNavigationOptions.Clamped);
}
}
示例 3:环绕导航与钳制导航
using AlicizaX.UI;
using RecyclerViewBookSamples;
public sealed class CircularNavigableRender : ItemRender<DemoSimpleData, DemoItemHolder>
{
public override ItemInteractionFlags InteractionFlags =>
ItemInteractionFlags.Select | ItemInteractionFlags.Move;
protected override RecyclerNavigationOptions NavigationOptions =>
RecyclerNavigationOptions.Circular; // 到尾部后回到头部
protected override void OnBind(DemoSimpleData data, int index)
{
Holder.title.text = data.Title;
}
}
列表入口与焦点恢复
示例 1:外部按钮把焦点送入列表
using AlicizaX.UI;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public sealed class EntryBridgeDemo : MonoBehaviour
{
[SerializeField] private Button entryButton;
[SerializeField] private RecyclerView recyclerView;
private void Awake()
{
entryButton.onClick.AddListener(() =>
{
EventSystem.current.SetSelectedGameObject(recyclerView.gameObject); // 选中桥节点
recyclerView.TryFocusEntry(MoveDirection.Down); // 然后进入列表
});
}
}
示例 2:重新打开面板时恢复上次项
using AlicizaX.UI;
using UnityEngine;
public sealed class NavigationRestoreDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private int lastIndex;
private void Awake()
{
recyclerView.OnIndexChanged += index => lastIndex = index; // 记录上次选中索引
}
private void OnEnable()
{
recyclerView.TryFocusIndex(lastIndex, smooth: true); // 平滑恢复焦点
}
}
示例 3:空列表时避免错误进入
using AlicizaX.UI;
using UnityEngine;
public sealed class EmptyNavigationGuardDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
public void TryEnter()
{
if (!recyclerView.TryFocusEntry(MoveDirection.Down))
{
Debug.Log("List is empty or not ready."); // 空列表不强行进入
}
}
}
业务包装层(UGList / UGListExtensions)
UGList 家族是官方提供的“业务友好包装层”,适合绝大多数游戏页面。它把适配器创建、内部绑定流程和常见滚动操作做了简化。
API 说明
类型:UGListBase<TData, TAdapter>
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/UGList.cs
| 成员/方法 | 类型/返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
RecyclerView |
RecyclerView |
构造注入 | 关联容器。 | 只读。 |
Adapter |
TAdapter |
构造注入 | 关联适配器。 | 只读。 |
Data |
List<TData> |
null |
当前数据集合。 | 赋值时会调用 _adapter.SetList()。 |
RegisterItemRender<TItemRender>(string viewName = "") |
void |
viewName="" |
注册渲染器。 | 空字符串表示默认模板。 |
RegisterItemRender(Type itemRenderType, string viewName = "") |
void |
viewName="" |
运行时注册渲染器。 | 类型需继承 ItemRenderBase。 |
UnregisterItemRender(string viewName = "") |
bool |
viewName="" |
注销渲染器。 | 成功返回 true。 |
ClearItemRenderRegistrations() |
void |
无 | 清空注册。 | 适合切换整套模板。 |
类型:具体包装类
| 类型 | 适配器类型 | 说明 | 使用限制 |
|---|---|---|---|
UGList<TData> |
Adapter<TData> |
单模板普通列表。 | TData : ISimpleViewData |
UGGroupList<TData> |
GroupAdapter<TData> |
分组列表。 | TData : class, IGroupViewData, new() |
UGLoopList<TData> |
LoopAdapter<TData> |
循环列表。 | TData : ISimpleViewData, new() |
UGMixedList<TData> |
MixedAdapter<TData> |
多模板列表。 | TData : IMixedViewData |
类型:UGListCreateHelper
| 方法 | 返回值 | 默认参数 | 说明 | 使用限制 |
|---|---|---|---|---|
Create<TData>(RecyclerView recyclerView) |
UGList<TData> |
无 | 创建普通列表包装。 | TData : ISimpleViewData |
CreateGroup<TData>(RecyclerView recyclerView, string groupViewName) |
UGGroupList<TData> |
无 | 创建分组列表包装。 | 必须提供组头模板名。 |
CreateLoop<TData>(RecyclerView recyclerView) |
UGLoopList<TData> |
无 | 创建循环列表包装。 | TData : ISimpleViewData, new() |
CreateMixed<TData>(RecyclerView recyclerView) |
UGMixedList<TData> |
无 | 创建多模板列表包装。 | TData : IMixedViewData |
类型:UGListExtensions
源码位置:Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/UGListExtensions.cs
| 成员/方法 | 返回值 | 默认值 | 说明 | 使用限制 |
|---|---|---|---|---|
DebugScrollTo |
bool |
false |
输出滚动定位日志。 | 仅用于调试。 |
ScrollTo(...) |
void |
alignment=Start, offset=0f, smooth=false, duration=0.3f |
包装 RecyclerView.ScrollToWithAlignment()。 |
ugList.RecyclerView 为空时打印警告。 |
ScrollToStart(...) |
void |
offset=0f, smooth=false, duration=0.3f |
滚到起始端。 | 同上。 |
ScrollToCenter(...) |
void |
offset=0f, smooth=false, duration=0.3f |
滚到中间。 | 同上。 |
ScrollToEnd(...) |
void |
offset=0f, smooth=false, duration=0.3f |
滚到末端。 | 同上。 |
快速创建与滚动扩展
示例 1:UGList 最简创建
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class QuickUgListDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGList<DemoSimpleData> list;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "快速创建 1" },
new DemoSimpleData { Title = "快速创建 2" },
};
}
}
示例 2:使用扩展方法滚动
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class UgListExtensionsDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGList<DemoSimpleData> list;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
}
public void CenterItem(int index)
{
list.ScrollToCenter(index, offset: 0f, smooth: true, duration: 0.2f); // 使用扩展方法
}
}
示例 3:打开滚动定位调试日志
using AlicizaX.UI;
using UnityEngine;
public sealed class UgListDebugDemo : MonoBehaviour
{
private void Awake()
{
UGListExtensions.DebugScrollTo = true; // 打开定位日志
}
}
业务封装示例
示例 1:业务侧封装统一入口
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class MailListFacade
{
private readonly UGList<DemoSimpleData> list;
public MailListFacade(RecyclerView recyclerView)
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
}
public void SetMails(List<DemoSimpleData> mails)
{
list.Data = mails; // 统一数据入口
}
public void FocusFirstUnread()
{
list.RecyclerView.TryFocusIndex(0, true, ScrollAlignment.Center);
}
}
示例 2:封装“刷新 + 保留焦点”
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
public sealed class FocusAwareFacade
{
private readonly UGList<DemoSimpleData> list;
private int lastIndex;
public FocusAwareFacade(RecyclerView recyclerView)
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
recyclerView.OnIndexChanged += index => lastIndex = index;
}
public void Replace(List<DemoSimpleData> data)
{
list.Data = data;
list.RecyclerView.TryFocusIndex(lastIndex, false); // 数据刷新后恢复焦点
}
}
场景专题
本节给出更贴近业务的完整场景脚本,重点覆盖性能与交互要求较高的页面。
大量列表项复用(含对象池配置)
示例 1:一万条数据的大列表
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class MassiveListDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: LinearLayoutManager + Scroller
private UGList<DemoSimpleData> list;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
var rows = new List<DemoSimpleData>(10000);
for (int i = 0; i < 10000; i++)
{
rows.Add(new DemoSimpleData
{
Title = $"战利品 {i + 1}",
Subtitle = $"ID = {100000 + i}",
});
}
list.Data = rows;
Debug.Log(recyclerView.PoolStats); // 观察命中、未命中和活跃数
}
}
示例 2:手工调对象池容量
using AlicizaX.UI;
using UnityEngine;
public sealed class MassiveListPoolConfigDemo : MonoBehaviour
{
[SerializeField] private DemoPoolHolder rowTemplate;
[SerializeField] private Transform poolRoot;
private ObjectPool<DemoPoolHolder> pool;
private void Awake()
{
pool = new ObjectPool<DemoPoolHolder>(
new DemoPoolFactory(rowTemplate, poolRoot),
initialSize: 24,
maxSize: 96); // 明确给出大列表池容量
pool.Warm(48); // 提前创建一部分
}
private void Start()
{
Debug.Log($"active={pool.ActiveCount}, inactive={pool.InactiveCount}, peak={pool.PeakActive}");
}
}
💡 大列表调优的首要目标不是“把所有项都放进池里”,而是让“可见项 + 一屏缓冲”命中率足够高。
多模板混排
示例 1:信息流混排
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class FeedCardHolder : ViewHolder
{
public UnityEngine.UI.Text title;
}
public sealed class FeedCardRender : ItemRender<DemoMixedData, FeedCardHolder>
{
protected override void OnBind(DemoMixedData data, int index)
{
Holder.title.text = $"[Card] {data.Title}";
}
}
public sealed class MixedFeedDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: MixedLayoutManager + 2 templates
private UGMixedList<DemoMixedData> list;
private void Start()
{
list = UGListCreateHelper.CreateMixed<DemoMixedData>(recyclerView);
list.RegisterItemRender<DemoMixedRender>("FeedText");
list.RegisterItemRender<FeedCardRender>("FeedCard");
list.Data = new List<DemoMixedData>
{
new DemoMixedData { TemplateName = "FeedCard", Title = "头图 Banner" },
new DemoMixedData { TemplateName = "FeedText", Title = "正文 1", Subtitle = "文本块" },
new DemoMixedData { TemplateName = "FeedText", Title = "正文 2", Subtitle = "文本块" },
new DemoMixedData { TemplateName = "FeedCard", Title = "推荐卡片" },
};
}
}
示例 2:未知模板名的保护策略
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class MixedTemplateGuardDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
public void BindSafely(List<DemoMixedData> rows)
{
foreach (DemoMixedData row in rows)
{
if (string.IsNullOrEmpty(row.TemplateName))
{
row.TemplateName = "FeedText"; // 回退到一个可用模板
}
}
var list = UGListCreateHelper.CreateMixed<DemoMixedData>(recyclerView);
list.RegisterItemRender<DemoMixedRender>("FeedText");
list.Data = rows;
}
}
⚠️
MixedViewProvider.GetTemplate()找不到模板名时会抛KeyNotFoundException;不要把兜底逻辑留给运行时异常。
分页加载(加载态、空态、错误态)
示例 1:统一状态流
using System;
using System.Collections;
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public abstract class PagedEntryData : IMixedViewData
{
public string TemplateName { get; set; }
}
public sealed class PagedContentData : PagedEntryData
{
public string Title;
}
public sealed class PagedStateData : PagedEntryData
{
public string Message;
public Action Retry;
}
public sealed class PagedContentRender : ItemRender<PagedContentData, DemoItemHolder>
{
protected override void OnBind(PagedContentData data, int index)
{
Holder.title.text = data.Title;
Holder.subtitle.text = $"Item {index}";
}
}
public sealed class PagedStateRender : ItemRender<PagedStateData, DemoStateHolder>
{
private Action retry;
protected override void OnHolderAttached()
{
base.OnHolderAttached();
Holder.retryButton.onClick.AddListener(OnRetryClicked); // 只注册一次
}
protected override void OnHolderDetached()
{
Holder.retryButton.onClick.RemoveListener(OnRetryClicked);
base.OnHolderDetached();
}
protected override void OnBind(PagedStateData data, int index)
{
retry = data.Retry;
Holder.message.text = data.Message;
Holder.retryButton.gameObject.SetActive(data.Retry != null); // 错误态显示重试按钮
}
protected override void OnClear()
{
retry = null; // 回收时清掉业务引用
}
private void OnRetryClicked()
{
retry?.Invoke();
}
}
public sealed class PagedLoadingDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: 需有 ContentItem / LoadingItem / EmptyItem / ErrorItem 模板
private UGMixedList<PagedEntryData> list;
private int page = 1;
private void Start()
{
list = UGListCreateHelper.CreateMixed<PagedEntryData>(recyclerView);
list.RegisterItemRender<PagedContentRender>("ContentItem");
list.RegisterItemRender<PagedStateRender>("LoadingItem");
list.RegisterItemRender<PagedStateRender>("EmptyItem");
list.RegisterItemRender<PagedStateRender>("ErrorItem");
StartCoroutine(LoadPage());
}
private IEnumerator LoadPage()
{
ShowLoading();
yield return new WaitForSeconds(0.5f); // 模拟网络
if (page == 1)
{
ShowContent(new List<PagedEntryData>
{
new PagedContentData { TemplateName = "ContentItem", Title = "第一页-1" },
new PagedContentData { TemplateName = "ContentItem", Title = "第一页-2" },
});
page++;
yield break;
}
ShowEmpty(); // 第二次请求模拟空态
}
private void ShowLoading()
{
list.Data = new List<PagedEntryData>
{
new PagedStateData { TemplateName = "LoadingItem", Message = "正在加载..." },
};
}
private void ShowContent(List<PagedEntryData> rows)
{
list.Data = rows;
}
private void ShowEmpty()
{
list.Data = new List<PagedEntryData>
{
new PagedStateData { TemplateName = "EmptyItem", Message = "暂无数据" },
};
}
public void ShowError()
{
list.Data = new List<PagedEntryData>
{
new PagedStateData
{
TemplateName = "ErrorItem",
Message = "加载失败,点击重试",
Retry = () => StartCoroutine(LoadPage()), // 错误态回调
},
};
}
}
示例 2:滚动到底部加载下一页
using AlicizaX.UI;
using UnityEngine;
public sealed class PagedLoadMoreDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private bool isLoading;
private void Awake()
{
recyclerView.OnScrollValueChanged += TryLoadMore; // 监听滚动变化
}
private void TryLoadMore()
{
if (isLoading || recyclerView.Scroller == null)
{
return;
}
float remain = recyclerView.Scroller.MaxPosition - recyclerView.GetScrollPosition();
if (remain > 120f)
{
return; // 距离底部还远,不加载
}
isLoading = true;
Debug.Log("Reach bottom, load next page.");
}
}
轮播与循环滚动(自动播放、手动切换)
示例 1:循环 Banner
using System.Collections;
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
using UnityEngine.UI;
public sealed class BannerCarouselDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView; // Inspector: PageLayoutManager + Snap = true
[SerializeField] private Button prevButton;
[SerializeField] private Button nextButton;
private UGLoopList<DemoSimpleData> list;
private int virtualIndex;
private Coroutine autoPlay;
private void Start()
{
list = UGListCreateHelper.CreateLoop<DemoSimpleData>(recyclerView);
list.RegisterItemRender<DemoSimpleRender>();
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "Banner A" },
new DemoSimpleData { Title = "Banner B" },
new DemoSimpleData { Title = "Banner C" },
};
virtualIndex = list.Adapter.GetRealCount() * 100; // 从中间起始,保证可双向滚
recyclerView.ScrollToWithAlignment(virtualIndex, ScrollAlignment.Center);
prevButton.onClick.AddListener(() => Move(-1)); // 手动上一张
nextButton.onClick.AddListener(() => Move(1)); // 手动下一张
autoPlay = StartCoroutine(AutoPlay());
}
private void OnDisable()
{
if (autoPlay != null)
{
StopCoroutine(autoPlay);
}
}
private IEnumerator AutoPlay()
{
while (true)
{
yield return new WaitForSeconds(3f);
Move(1); // 自动播放
}
}
private void Move(int step)
{
virtualIndex += step;
recyclerView.ScrollToWithAlignment(virtualIndex, ScrollAlignment.Center, 0f, true, 0.25f);
}
}
示例 2:非循环分页的边界保护
using AlicizaX.UI;
using UnityEngine;
public sealed class FiniteCarouselDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
[SerializeField] private int itemCount = 5;
private int currentIndex;
private void Awake()
{
recyclerView.OnIndexChanged += index => currentIndex = Mathf.Max(index, 0);
}
public void Next()
{
int target = Mathf.Min(currentIndex + 1, itemCount - 1);
recyclerView.ScrollToWithAlignment(target, ScrollAlignment.Center, 0f, true, 0.2f);
}
public void Prev()
{
int target = Mathf.Max(currentIndex - 1, 0);
recyclerView.ScrollToWithAlignment(target, ScrollAlignment.Center, 0f, true, 0.2f);
}
}
手柄/键盘导航结合列表滚动
示例 1:打开面板自动聚焦第一项
using System.Collections.Generic;
using AlicizaX.UI;
using RecyclerViewBookSamples;
using UnityEngine;
public sealed class GamepadListDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private UGList<DemoSimpleData> list;
private void Start()
{
list = UGListCreateHelper.Create<DemoSimpleData>(recyclerView);
list.RegisterItemRender<NavigableItemRender>();
list.Data = new List<DemoSimpleData>
{
new DemoSimpleData { Title = "图像" },
new DemoSimpleData { Title = "声音" },
new DemoSimpleData { Title = "操作" },
};
recyclerView.TryFocusIndex(0, false, ScrollAlignment.Start); // 初次进入聚焦第一项
}
}
示例 2:导航时自动把目标项滚进可见区
using AlicizaX.UI;
using UnityEngine;
public sealed class NavigationScrollSyncDemo : MonoBehaviour
{
[SerializeField] private RecyclerView recyclerView;
private int currentIndex;
private void Awake()
{
recyclerView.OnIndexChanged += index => currentIndex = Mathf.Max(index, 0);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.DownArrow))
{
recyclerView.TryFocusIndex(currentIndex + 1, smooth: true, alignment: ScrollAlignment.End);
}
if (Input.GetKeyDown(KeyCode.UpArrow))
{
recyclerView.TryFocusIndex(currentIndex - 1, smooth: true, alignment: ScrollAlignment.Start);
}
}
}
📌
TryFocusIndex(..., smooth: true)的意义不只是“滚动”,还包括“滚动结束后恢复焦点”。
FAQ
常见问题
Q1:为什么我已经赋值了数据,但列表里什么都没有?
- 最常见原因是没有通过
UGList.Data、Adapter.SetList()或Notify*()等正式入口触发列表更新。 - 其次是
Templates为空、LayoutManager未配置或模板名与TemplateName不匹配。 - 如果是多模板列表,还要确认已经为每个模板注册了
ItemRender。
Q2:为什么 NotifyItemChanged() 后界面没变?
- 只会重绑“当前可见”的 Holder;不可见项会等滚动到可见区后再更新。
- 如果该项高度发生变化,需要用
NotifyItemChanged(index, true)。
Q3:为什么循环列表里业务索引和 Holder.Index 不一样?
OnIndexChanged回调参数与Adapter.ChoiceIndex代表的是业务层关注的“真实索引”。Holder.Index是布局层的“虚拟索引”。LoopAdapter绑定时会对虚拟索引取模。
Q4:为什么混排列表报模板找不到?
MixedAdapter.GetViewName()直接返回TemplateName。MixedViewProvider用模板对象的name做键。- 任何大小写、空格或重命名不一致都会导致失败。
Q5:为什么滚动条不显示?
showScrollBar或Scroll可能未打开。ShowScrollBarOnlyWhenScrollable = true时,内容未超过视口就不会显示。Direction.Custom不支持当前实现的溢出检测。
Q6:为什么分页吸附不准确?
PageLayoutManager最适合固定尺寸页面。- 项尺寸变化大时应换
MixedLayoutManager,并自己定义滚动定位策略。
Q7:为什么切换页面后焦点丢了?
- 视图复用会销毁/回收之前的
Selectable。 - 应该在
OnEnable()或刷新完成后调用TryFocusIndex()。
Q8:为什么对象池命中率低?
- 预热量不足。
- 列表每次都被
Reset()。 - 使用了全量刷新替代差量刷新。
Anti-patterns
常见反模式与替代方案
| 反模式 | 问题 | 正确做法 |
|---|---|---|
每次数据变化都调用 NotifyDataChanged() |
频繁清空并重建可见项,浪费池命中率。 | 优先使用 NotifyItemChanged() / NotifyItemRangeChanged()。 |
多模板列表里把 TemplateName 写成业务文案 |
模板名一旦翻译或重命名就失效。 | 用固定常量,如 const string FeedCard = "FeedCard";。 |
在 OnBind() 里重复注册按钮事件 |
Holder 被复用后会出现重复回调。 | 在 OnHolderAttached() 注册,在 OnHolderDetached() 解绑。 |
直接改 Scroller.Position 期待界面自动更新 |
位置变了但布局未刷新。 | 优先调用 RecyclerView.ScrollTo*()。 |
列表数据尚未通过正式入口完成更新就调用 TryFocusIndex() |
焦点目标尚未生成。 | 先写入 Data / SetList() / Notify*(),必要时延后到下一帧或 OnEnable()。 |
使用 LoopAdapter 却仍以真实索引做“下一页” |
会反复跳回前几项。 | 维护独立的虚拟索引。 |
| 每次打开页面都重新 new 包装层与数据容器 | 造成 GC 抖动和池统计失真。 | 页面级持有 UGList / Adapter,只替换数据。 |
| 把“加载中/空态/错误态”放在列表外部多套 UI | 逻辑分裂、切换难维护。 | 用 MixedAdapter 把状态也当作一种 Item。 |
性能优化建议
建议清单
- 视图池预热:首屏数据尽量一次性设置到位,内部布局流程会自动触发
PreparePool()准备“可见项 + 缓冲项”。 - 优先差量刷新:文本、数值、开关状态变化优先用
NotifyItemChanged()/NotifyItemRangeChanged()。 - 避免全量刷新:
NotifyDataChanged()只用于排序、模板切换、尺寸大量变化等无法局部更新的情况。 - 复用业务包装层:面板级缓存
UGList/Adapter,不要每次打开都重建。 - 控制模板数量:多模板会降低池命中率;能合并的样式尽量合并。
- 固定 Item 尺寸优先:能用
LinearLayoutManager/GridLayoutManager时不要上来就用MixedLayoutManager。 - 减少
OnBind()重活:不要在OnBind()里做同步大图加载、复杂字符串拼接或深层级查找。 - 缓存引用:
Holder上的Text、Image、Button在模板初始化时就拖好,避免运行时GetComponent()。 - 记录峰值活跃数:观察
PoolStats、PeakActive、GetPeakActiveCount(),按峰值 + buffer 设置池容量。 - 滚动事件节流:
OnScrollValueChanged是高频回调,尽量只做阈值判断,不做昂贵计算。 - 大量分页时保留数据容器:分页追加优先
AddRange(),避免整页复制新List<T>。 - 减少
Reset():SetList()会触发RecyclerView.Reset();如果只是局部追加,直接对适配器做增量操作更稳。
💡 一个常见经验值:池容量先按“单屏可见数 + 1 屏缓冲”估,再通过
PeakActive微调,而不是拍脑袋设置超大容量。
⚠️ 如果列表项高度经常变化,却还持续使用
NotifyItemChanged(index, false),最终会出现错位、重叠或滚动条比例异常。
交付前检查清单
Templates已配置,且多模板名称与TemplateName完全一致。LayoutManager与Scroller已在 Inspector 上正确绑定。- 已为所有模板注册对应
ItemRender。 - 数据赋值后已通过
Data/SetList()/Notify*()触发内部布局与刷新流程。 - 差量更新优先使用
NotifyItemChanged()/NotifyItemRangeChanged()。 - 焦点型页面已验证
TryFocusIndex()、TryFocusEntry()与关闭/重开流程。 - 大列表页面已检查
PoolStats/PeakActive/GetPeakActiveCount()。 - 加载态、空态、错误态已验证,且不会因模板缺失报错。
- 循环列表已使用虚拟索引,不直接拿真实索引做连续滚动。
OnBind()中没有重复注册事件,也没有未释放的临时引用。