# 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 序列化引用配置,业务代码通常只“读取与使用”,不建议在运行中直接替换类型。 ## 目录 - [模块概览](#overview) - [基础概念与共享示例类型](#basics) - [容器(RecyclerView)](#container) - [API 说明](#container-api) - [初始化与刷新](#container-init) - [定位与焦点控制](#container-focus) - [适配器(Adapter / GroupAdapter / LoopAdapter / MixedAdapter)](#adapter) - [API 说明](#adapter-api) - [基础数据更新](#adapter-data) - [多模板、分组与循环](#adapter-advanced) - [布局(LayoutManager 系列)](#layout) - [API 说明](#layout-api) - [线性与网格布局](#layout-basic) - [分页、圆环与异构长度布局](#layout-advanced) - [滚动器(Scroller / CircleScroller)](#scroller) - [API 说明](#scroller-api) - [程序化滚动](#scroller-programmatic) - [惯性、吸附与自动播放](#scroller-advanced) - [视图池(ViewProvider / ObjectPool)](#pool) - [API 说明](#pool-api) - [预热与复用](#pool-basic) - [多模板对象池配置](#pool-mixed) - [导航(RecyclerNavigation)](#navigation) - [API 说明](#navigation-api) - [列表内导航](#navigation-inner) - [列表入口与焦点恢复](#navigation-entry) - [业务包装层(UGList / UGListExtensions)](#uglist) - [API 说明](#uglist-api) - [快速创建与滚动扩展](#uglist-basic) - [业务封装示例](#uglist-advanced) - [场景专题](#scenarios) - [大量列表项复用(含对象池配置)](#scenario-massive) - [多模板混排](#scenario-mixed) - [分页加载(加载态、空态、错误态)](#scenario-paging) - [轮播与循环滚动(自动播放、手动切换)](#scenario-carousel) - [手柄/键盘导航结合列表滚动](#scenario-nav-scroll) - [FAQ](#faq) - [Anti-patterns](#anti-patterns) - [性能优化建议](#performance) - [交付前检查清单](#checklist) ## 模块概览 `RecyclerView` 是一套面向 Unity UGUI 的高性能列表组件,职责拆分如下: - 容器:`RecyclerView` 负责模板、可见区、滚动同步、滚动条、焦点恢复。 - 适配器:`Adapter` 家族负责数据量、模板选择、视图绑定、局部刷新。 - 布局:`LayoutManager` 家族负责位置计算、可见区间与索引转换。 - 滚动器:`Scroller` 家族负责拖拽、滚轮、惯性、吸附与平滑滚动。 - 视图池:`ViewProvider` + `ObjectPool` 负责创建、回收、预热与统计。 - 导航:`RecyclerNavigationController` 家族负责手柄/键盘在列表内的可达性。 - 业务包装层:`UGList` 家族负责降低泛型与注册样板代码。 > 💡 一般调用顺序是:准备模板与布局 → 创建业务包装层/适配器包装 → 注册 `ItemRender` → 赋值数据;后续布局计算、池预热与首屏刷新由框架自动完成。 > ⚠️ 如果 `Templates` 为空、`LayoutManager` 未配置或 `Content` 解析失败,`RecyclerView` 会在首次运行时抛出错误或记录错误日志。 ## 基础概念与共享示例类型 本节给出后续示例默认共用的最小类型。除非某个示例额外声明了自己的 `Data` / `Holder` / `Render`,否则都可以直接与以下代码一起编译。 ### 示例基础类型 #### 共享类型代码 ```csharp 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 { 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 { 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` | `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:基础纵向列表初始化 ```csharp 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 list; private void Awake() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); // 注册默认模板渲染器 list.Data = new List { new DemoSimpleData { Title = "邮件", Subtitle = "今天 09:00" }, new DemoSimpleData { Title = "任务", Subtitle = "今天 10:30" }, new DemoSimpleData { Title = "公告", Subtitle = "今天 14:00" }, }; } } ``` #### 示例 2:异步拉取后再刷新容器 ```csharp 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 list; private Adapter adapter; private IEnumerator Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); adapter = list.Adapter; yield return new WaitForSeconds(0.5f); // 模拟网络请求 list.Data = new List { new DemoSimpleData { Title = "远程数据 A", Subtitle = "加载完成" }, new DemoSimpleData { Title = "远程数据 B", Subtitle = "加载完成" }, }; // 赋值后框架自动布局并刷新 } } ``` #### 示例 3:空数据与安全刷新 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class EmptyStateRecyclerViewDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; private UGList list; private void OnEnable() { list ??= UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); list.Data = new List(); // 空列表也是合法输入 bool focused = recyclerView.TryFocusIndex(0); // 空列表会返回 false Debug.Log($"Focus result: {focused}"); } } ``` ### 定位与焦点控制 #### 示例 1:按钮驱动滚动到指定项 ```csharp 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:打开面板时恢复焦点 ```csharp 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:监听焦点索引变化 ```csharp 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`(`where T : ISimpleViewData`) 源码位置:`Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/Adapter/Adapter.cs` **构造与状态** | 成员名 | 类型 | 默认值 | 说明 | 使用限制 | | --- | --- | --- | --- | --- | | `Adapter(RecyclerView recyclerView)` | 构造函数 | `list = new List()` | 创建空适配器。 | `recyclerView` 不能为空。 | | `Adapter(RecyclerView recyclerView, List list)` | 构造函数 | `list ?? new List()` | 创建带初始数据的适配器。 | 传入 `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 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(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 collection)` | `void` | 无 | 批量追加。 | `collection == null` 时忽略。 | | `Insert(int index, T item)` | `void` | 无 | 指定位置插入。 | 越界由 `List` 自己抛错。 | | `InsertRange(int index, IEnumerable collection)` | `void` | 无 | 批量插入。 | `collection == null` 时忽略。 | | `Remove(T item)` | `void` | 无 | 删除首个匹配项。 | 找不到时等价于 `RemoveAt(-1)`,最终无效果。 | | `RemoveAt(int index)` | `void` | 无 | 删除指定索引。 | 越界直接返回。 | | `RemoveRange(int index, int count)` | `void` | 无 | 删除区间。 | 参数必须满足 `List.RemoveRange` 要求。 | | `RemoveAll(Predicate match)` | `void` | 无 | 条件删除。 | 始终触发全量刷新。 | | `Clear()` | `void` | 无 | 清空列表。 | 空列表时直接返回。 | | `Reverse()` / `Reverse(int index, int count)` | `void` | 无 | 反转顺序。 | 最终触发全量刷新。 | | `Sort(Comparison comparison)` | `void` | 无 | 排序。 | 最终触发全量刷新。 | #### 类型:`GroupAdapter`(`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` | 方法/成员 | 返回值 | 默认值 | 说明 | 使用限制 | | --- | --- | --- | --- | --- | | `GetItemCount()` | `int` | 无 | 有数据时返回 `int.MaxValue`。 | 只适合循环/轮播场景。 | | `GetRealCount()` | `int` | 无 | 返回真实数据量。 | 业务逻辑应优先使用此值。 | | `OnBindViewHolder(...)` | `void` | 无 | 绑定时对索引做取模。 | 数据为空时不绑定。 | #### 类型:`MixedAdapter`(`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` 进行增删改 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class AdapterCrudDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; private UGList list; private Adapter adapter; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); list.Data = new List(); 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:差量刷新单项与区间 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class AdapterPartialRefreshDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; private UGList list; private Adapter adapter; private readonly List 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(recyclerView); list.RegisterItemRender(); 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:尺寸变化时强制重布局 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class AdapterRelayoutDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; private UGList list; private Adapter adapter; private readonly List data = new(); private void Start() { data.Add(new DemoSimpleData { Title = "短文本", Subtitle = "1 行" }); list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); list.Data = data; adapter = list.Adapter; } public void ExpandText() { data[0].Subtitle = "这一条文本被拉长后可能导致高度变化,因此需要 relayout=true。"; adapter.NotifyItemChanged(0, relayout: true); // 尺寸变化时必须重布局 } } ``` ### 多模板、分组与循环 #### 示例 1:多模板注册与绑定 ```csharp 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 { 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 list; private void Start() { list = UGListCreateHelper.CreateMixed(recyclerView); list.RegisterItemRender("SmallItem"); // 绑定小模板 list.RegisterItemRender("LargeItem"); // 绑定大模板 list.Data = new List { new DemoMixedData { TemplateName = "LargeItem", Title = "头图", Subtitle = "大卡片" }, new DemoMixedData { TemplateName = "SmallItem", Title = "正文 A", Subtitle = "小卡片" }, new DemoMixedData { TemplateName = "SmallItem", Title = "正文 B", Subtitle = "小卡片" }, }; } } ``` #### 示例 2:分组展开/收起 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class GroupHeaderRender : ItemRender { 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 { 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 list; private void Start() { list = UGListCreateHelper.CreateGroup(recyclerView, "GroupHeader"); list.RegisterItemRender("GroupHeader"); list.RegisterItemRender("GroupItem"); list.Data = new List { 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:循环适配器的虚拟索引 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class LoopAdapterDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; private UGLoopList list; private int virtualIndex; private void Start() { list = UGListCreateHelper.CreateLoop(recyclerView); list.RegisterItemRender(); list.Data = new List { 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:线性列表 ```csharp 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 list; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); list.Data = new List { new DemoSimpleData { Title = "线性列表 1" }, new DemoSimpleData { Title = "线性列表 2" }, new DemoSimpleData { Title = "线性列表 3" }, }; } } ``` #### 示例 2:网格背包 ```csharp 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 list; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); var items = new List(); for (int i = 0; i < 20; i++) { items.Add(new DemoSimpleData { Title = $"格子 {i + 1}", Subtitle = $"Index={i}" }); } list.Data = items; } } ``` #### 示例 3:空网格的边界结果 ```csharp 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(recyclerView); list.RegisterItemRender(); list.Data = new List(); // 空网格 } } ``` ### 分页、圆环与异构长度布局 #### 示例 1:分页卡片 ```csharp 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 list; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); list.Data = new List { new DemoSimpleData { Title = "Page 1" }, new DemoSimpleData { Title = "Page 2" }, new DemoSimpleData { Title = "Page 3" }, }; } } ``` #### 示例 2:圆环菜单 ```csharp 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 list; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); list.Data = new List { new DemoSimpleData { Title = "装备" }, new DemoSimpleData { Title = "技能" }, new DemoSimpleData { Title = "任务" }, new DemoSimpleData { Title = "地图" }, }; } } ``` #### 示例 3:异构长度列表 ```csharp 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 list; private void Start() { list = UGListCreateHelper.CreateMixed(recyclerView); list.RegisterItemRender("SmallItem"); list.RegisterItemRender("LargeItem"); // 两个模板尺寸不同 list.Data = new List { 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:立即滚动与平滑滚动 ```csharp 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:按固定时长滚动 ```csharp 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:无滚动能力时的防御式调用 ```csharp 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:分页列表的吸附体验 ```csharp 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:自动播放轮播 ```csharp 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:手动上一页 / 下一页 ```csharp 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` | 空集合 | 当前可见 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` | `initialSize = 0`, `maxSize = 1` | 单模板池。 | 实际容量会在 `PreparePool()` 中扩容。 | | `PoolStats` | `string` | 实时统计 | 命中、未命中、销毁、活跃等信息。 | 文本格式仅用于日志。 | #### 类型:`MixedViewProvider` | 成员 | 类型 | 默认值 | 说明 | 使用限制 | | --- | --- | --- | --- | --- | | 内部池 | `MixedObjectPool` | 每类型默认上限 `10` | 多模板池。 | 模板名必须唯一。 | | `PoolStats` | `string` | 实时统计 | 当前命中、未命中、销毁信息。 | 细粒度活跃数需看 `MixedObjectPool`。 | #### 类型:`ObjectPool` 源码位置:`Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/ObjectPool/ObjectPool.cs` | 方法/成员 | 返回值/类型 | 默认值 | 说明 | 使用限制 | | --- | --- | --- | --- | --- | | `ObjectPool(IObjectFactory factory)` | 构造函数 | `maxSize = Environment.ProcessorCount * 2` | 用 CPU 数量推导上限。 | 初始大小为 `0`。 | | `ObjectPool(IObjectFactory factory, int maxSize)` | 构造函数 | `initialSize = 0` | 指定最大池容量。 | `maxSize` 需大于等于 `0`。 | | `ObjectPool(IObjectFactory 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` | 方法/成员 | 返回值/类型 | 默认值 | 说明 | 使用限制 | | --- | --- | --- | --- | --- | | `MixedObjectPool(IMixedObjectFactory factory)` | 构造函数 | `defaultMaxSizePerType = 10` | 使用默认每类型容量。 | `factory` 不能为空。 | | `MixedObjectPool(IMixedObjectFactory 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()` 自动预热 ```csharp 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(recyclerView); list.RegisterItemRender(); var data = new List(); for (int i = 0; i < 200; i++) { data.Add(new DemoSimpleData { Title = $"行 {i + 1}" }); } list.Data = data; Debug.Log(recyclerView.PoolStats); // 输出当前池统计 } } ``` #### 示例 2:直接使用 `ObjectPool` 自定义容量 ```csharp using AlicizaX.UI; using UnityEngine; using UnityEngine.UI; public sealed class DemoPoolHolder : ViewHolder { public Text title; } public sealed class DemoPoolFactory : IObjectFactory { 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 pool; private void Awake() { pool = new ObjectPool(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:校验失败时自动销毁 ```csharp using AlicizaX.UI; using UnityEngine; public sealed class PoolValidateDemo : MonoBehaviour { [SerializeField] private DemoPoolHolder template; [SerializeField] private Transform parent; private ObjectPool pool; private void Start() { pool = new ObjectPool(new DemoPoolFactory(template, parent), 1, 2); DemoPoolHolder holder = pool.Allocate(); Destroy(holder.gameObject); // 模拟外部错误销毁 pool.Free(holder); // Validate 失败后不会重新入池 } } ``` ### 多模板对象池配置 #### 示例 1:按模板类型分别预热 ```csharp 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 pool; private void Awake() { var templates = new Dictionary { ["SmallItem"] = smallTemplate, ["LargeItem"] = largeTemplate, }; pool = new MixedObjectPool( new UnityMixedComponentFactory(templates, parent), defaultMaxSizePerType: 6); pool.EnsureCapacity("SmallItem", 24); // 小卡片出现频率更高 pool.EnsureCapacity("LargeItem", 8); // 大卡片数量更少 pool.Warm("SmallItem", 16); pool.Warm("LargeItem", 4); } } ``` #### 示例 2:根据峰值活跃数回写容量 ```csharp using AlicizaX.UI; using UnityEngine; public sealed class MixedPoolTuningDemo : MonoBehaviour { public void PrintRecommendation(MixedObjectPool 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 启用点击、移动与提交 ```csharp using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; using UnityEngine.EventSystems; public sealed class NavigableItemRender : ItemRender { 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:主动驱动导航器移动 ```csharp 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(); if (current == null) { recyclerView.TryFocusIndex(currentIndex, false, ScrollAlignment.Center); return; } recyclerView.NavigationController.TryMove(current, MoveDirection.Right, RecyclerNavigationOptions.Clamped); } } ``` #### 示例 3:环绕导航与钳制导航 ```csharp using AlicizaX.UI; using RecyclerViewBookSamples; public sealed class CircularNavigableRender : ItemRender { 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:外部按钮把焦点送入列表 ```csharp 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:重新打开面板时恢复上次项 ```csharp 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:空列表时避免错误进入 ```csharp 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` 源码位置:`Packages/com.alicizax.unity.ui.extension/Runtime/RecyclerView/UGList.cs` | 成员/方法 | 类型/返回值 | 默认值 | 说明 | 使用限制 | | --- | --- | --- | --- | --- | | `RecyclerView` | `RecyclerView` | 构造注入 | 关联容器。 | 只读。 | | `Adapter` | `TAdapter` | 构造注入 | 关联适配器。 | 只读。 | | `Data` | `List` | `null` | 当前数据集合。 | 赋值时会调用 `_adapter.SetList()`。 | | `RegisterItemRender(string viewName = "")` | `void` | `viewName=""` | 注册渲染器。 | 空字符串表示默认模板。 | | `RegisterItemRender(Type itemRenderType, string viewName = "")` | `void` | `viewName=""` | 运行时注册渲染器。 | 类型需继承 `ItemRenderBase`。 | | `UnregisterItemRender(string viewName = "")` | `bool` | `viewName=""` | 注销渲染器。 | 成功返回 `true`。 | | `ClearItemRenderRegistrations()` | `void` | 无 | 清空注册。 | 适合切换整套模板。 | #### 类型:具体包装类 | 类型 | 适配器类型 | 说明 | 使用限制 | | --- | --- | --- | --- | | `UGList` | `Adapter` | 单模板普通列表。 | `TData : ISimpleViewData` | | `UGGroupList` | `GroupAdapter` | 分组列表。 | `TData : class, IGroupViewData, new()` | | `UGLoopList` | `LoopAdapter` | 循环列表。 | `TData : ISimpleViewData, new()` | | `UGMixedList` | `MixedAdapter` | 多模板列表。 | `TData : IMixedViewData` | #### 类型:`UGListCreateHelper` | 方法 | 返回值 | 默认参数 | 说明 | 使用限制 | | --- | --- | --- | --- | --- | | `Create(RecyclerView recyclerView)` | `UGList` | 无 | 创建普通列表包装。 | `TData : ISimpleViewData` | | `CreateGroup(RecyclerView recyclerView, string groupViewName)` | `UGGroupList` | 无 | 创建分组列表包装。 | 必须提供组头模板名。 | | `CreateLoop(RecyclerView recyclerView)` | `UGLoopList` | 无 | 创建循环列表包装。 | `TData : ISimpleViewData, new()` | | `CreateMixed(RecyclerView recyclerView)` | `UGMixedList` | 无 | 创建多模板列表包装。 | `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` 最简创建 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class QuickUgListDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; private UGList list; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); list.Data = new List { new DemoSimpleData { Title = "快速创建 1" }, new DemoSimpleData { Title = "快速创建 2" }, }; } } ``` #### 示例 2:使用扩展方法滚动 ```csharp using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class UgListExtensionsDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; private UGList list; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); } public void CenterItem(int index) { list.ScrollToCenter(index, offset: 0f, smooth: true, duration: 0.2f); // 使用扩展方法 } } ``` #### 示例 3:打开滚动定位调试日志 ```csharp using AlicizaX.UI; using UnityEngine; public sealed class UgListDebugDemo : MonoBehaviour { private void Awake() { UGListExtensions.DebugScrollTo = true; // 打开定位日志 } } ``` ### 业务封装示例 #### 示例 1:业务侧封装统一入口 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class MailListFacade { private readonly UGList list; public MailListFacade(RecyclerView recyclerView) { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); } public void SetMails(List mails) { list.Data = mails; // 统一数据入口 } public void FocusFirstUnread() { list.RecyclerView.TryFocusIndex(0, true, ScrollAlignment.Center); } } ``` #### 示例 2:封装“刷新 + 保留焦点” ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; public sealed class FocusAwareFacade { private readonly UGList list; private int lastIndex; public FocusAwareFacade(RecyclerView recyclerView) { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); recyclerView.OnIndexChanged += index => lastIndex = index; } public void Replace(List data) { list.Data = data; list.RecyclerView.TryFocusIndex(lastIndex, false); // 数据刷新后恢复焦点 } } ``` ## 场景专题 本节给出更贴近业务的完整场景脚本,重点覆盖性能与交互要求较高的页面。 ### 大量列表项复用(含对象池配置) #### 示例 1:一万条数据的大列表 ```csharp 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 list; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); var rows = new List(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:手工调对象池容量 ```csharp using AlicizaX.UI; using UnityEngine; public sealed class MassiveListPoolConfigDemo : MonoBehaviour { [SerializeField] private DemoPoolHolder rowTemplate; [SerializeField] private Transform poolRoot; private ObjectPool pool; private void Awake() { pool = new ObjectPool( 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:信息流混排 ```csharp 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 { 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 list; private void Start() { list = UGListCreateHelper.CreateMixed(recyclerView); list.RegisterItemRender("FeedText"); list.RegisterItemRender("FeedCard"); list.Data = new List { 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:未知模板名的保护策略 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class MixedTemplateGuardDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; public void BindSafely(List rows) { foreach (DemoMixedData row in rows) { if (string.IsNullOrEmpty(row.TemplateName)) { row.TemplateName = "FeedText"; // 回退到一个可用模板 } } var list = UGListCreateHelper.CreateMixed(recyclerView); list.RegisterItemRender("FeedText"); list.Data = rows; } } ``` > ⚠️ `MixedViewProvider.GetTemplate()` 找不到模板名时会抛 `KeyNotFoundException`;不要把兜底逻辑留给运行时异常。 ### 分页加载(加载态、空态、错误态) #### 示例 1:统一状态流 ```csharp 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 { protected override void OnBind(PagedContentData data, int index) { Holder.title.text = data.Title; Holder.subtitle.text = $"Item {index}"; } } public sealed class PagedStateRender : ItemRender { 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 list; private int page = 1; private void Start() { list = UGListCreateHelper.CreateMixed(recyclerView); list.RegisterItemRender("ContentItem"); list.RegisterItemRender("LoadingItem"); list.RegisterItemRender("EmptyItem"); list.RegisterItemRender("ErrorItem"); StartCoroutine(LoadPage()); } private IEnumerator LoadPage() { ShowLoading(); yield return new WaitForSeconds(0.5f); // 模拟网络 if (page == 1) { ShowContent(new List { new PagedContentData { TemplateName = "ContentItem", Title = "第一页-1" }, new PagedContentData { TemplateName = "ContentItem", Title = "第一页-2" }, }); page++; yield break; } ShowEmpty(); // 第二次请求模拟空态 } private void ShowLoading() { list.Data = new List { new PagedStateData { TemplateName = "LoadingItem", Message = "正在加载..." }, }; } private void ShowContent(List rows) { list.Data = rows; } private void ShowEmpty() { list.Data = new List { new PagedStateData { TemplateName = "EmptyItem", Message = "暂无数据" }, }; } public void ShowError() { list.Data = new List { new PagedStateData { TemplateName = "ErrorItem", Message = "加载失败,点击重试", Retry = () => StartCoroutine(LoadPage()), // 错误态回调 }, }; } } ``` #### 示例 2:滚动到底部加载下一页 ```csharp 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 ```csharp 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 list; private int virtualIndex; private Coroutine autoPlay; private void Start() { list = UGListCreateHelper.CreateLoop(recyclerView); list.RegisterItemRender(); list.Data = new List { 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:非循环分页的边界保护 ```csharp 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:打开面板自动聚焦第一项 ```csharp using System.Collections.Generic; using AlicizaX.UI; using RecyclerViewBookSamples; using UnityEngine; public sealed class GamepadListDemo : MonoBehaviour { [SerializeField] private RecyclerView recyclerView; private UGList list; private void Start() { list = UGListCreateHelper.Create(recyclerView); list.RegisterItemRender(); list.Data = new List { new DemoSimpleData { Title = "图像" }, new DemoSimpleData { Title = "声音" }, new DemoSimpleData { Title = "操作" }, }; recyclerView.TryFocusIndex(0, false, ScrollAlignment.Start); // 初次进入聚焦第一项 } } ``` #### 示例 2:导航时自动把目标项滚进可见区 ```csharp 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`。 - 减少 `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()` 中没有重复注册事件,也没有未释放的临时引用。