2403 lines
89 KiB
Markdown
2403 lines
89 KiB
Markdown
|
|
# 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)
|
|||
|
|
|
|||
|
|
<a id="overview"></a>
|
|||
|
|
## 模块概览
|
|||
|
|
|
|||
|
|
`RecyclerView` 是一套面向 Unity UGUI 的高性能列表组件,职责拆分如下:
|
|||
|
|
|
|||
|
|
- 容器:`RecyclerView` 负责模板、可见区、滚动同步、滚动条、焦点恢复。
|
|||
|
|
- 适配器:`Adapter<T>` 家族负责数据量、模板选择、视图绑定、局部刷新。
|
|||
|
|
- 布局:`LayoutManager` 家族负责位置计算、可见区间与索引转换。
|
|||
|
|
- 滚动器:`Scroller` 家族负责拖拽、滚轮、惯性、吸附与平滑滚动。
|
|||
|
|
- 视图池:`ViewProvider` + `ObjectPool` 负责创建、回收、预热与统计。
|
|||
|
|
- 导航:`RecyclerNavigationController` 家族负责手柄/键盘在列表内的可达性。
|
|||
|
|
- 业务包装层:`UGList` 家族负责降低泛型与注册样板代码。
|
|||
|
|
|
|||
|
|
> 💡 一般调用顺序是:准备模板与布局 → 创建业务包装层/适配器包装 → 注册 `ItemRender` → 赋值数据;后续布局计算、池预热与首屏刷新由框架自动完成。
|
|||
|
|
|
|||
|
|
> ⚠️ 如果 `Templates` 为空、`LayoutManager` 未配置或 `Content` 解析失败,`RecyclerView` 会在首次运行时抛出错误或记录错误日志。
|
|||
|
|
|
|||
|
|
<a id="basics"></a>
|
|||
|
|
## 基础概念与共享示例类型
|
|||
|
|
|
|||
|
|
本节给出后续示例默认共用的最小类型。除非某个示例额外声明了自己的 `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<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` 等引用拖好。
|
|||
|
|
|
|||
|
|
<a id="container"></a>
|
|||
|
|
## 容器(RecyclerView)
|
|||
|
|
|
|||
|
|
`RecyclerView` 是系统的中心节点:它管理模板、内容区域、当前适配器、布局、滚动器、滚动条以及焦点导航。
|
|||
|
|
|
|||
|
|
<a id="container-api"></a>
|
|||
|
|
### 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()`。
|
|||
|
|
|
|||
|
|
<a id="container-init"></a>
|
|||
|
|
### 初始化与刷新
|
|||
|
|
|
|||
|
|
#### 示例 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<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:异步拉取后再刷新容器
|
|||
|
|
```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<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:空数据与安全刷新
|
|||
|
|
```csharp
|
|||
|
|
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}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="container-focus"></a>
|
|||
|
|
### 定位与焦点控制
|
|||
|
|
|
|||
|
|
#### 示例 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}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="adapter"></a>
|
|||
|
|
## 适配器(Adapter / GroupAdapter / LoopAdapter / MixedAdapter)
|
|||
|
|
|
|||
|
|
适配器负责把数据对象映射为模板名、绑定流程和局部刷新操作,是业务层最常直接扩展的模块。
|
|||
|
|
|
|||
|
|
<a id="adapter-api"></a>
|
|||
|
|
### 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`。
|
|||
|
|
|
|||
|
|
<a id="adapter-data"></a>
|
|||
|
|
### 基础数据更新
|
|||
|
|
|
|||
|
|
#### 示例 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<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:差量刷新单项与区间
|
|||
|
|
```csharp
|
|||
|
|
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:尺寸变化时强制重布局
|
|||
|
|
```csharp
|
|||
|
|
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); // 尺寸变化时必须重布局
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="adapter-advanced"></a>
|
|||
|
|
### 多模板、分组与循环
|
|||
|
|
|
|||
|
|
#### 示例 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<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:分组展开/收起
|
|||
|
|
```csharp
|
|||
|
|
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:循环适配器的虚拟索引
|
|||
|
|
```csharp
|
|||
|
|
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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="layout"></a>
|
|||
|
|
## 布局(LayoutManager 系列)
|
|||
|
|
|
|||
|
|
布局器只负责“算位置”,不负责数据绑定。它决定内容尺寸、可见区间、索引到坐标的映射以及吸附基准。
|
|||
|
|
|
|||
|
|
<a id="layout-api"></a>
|
|||
|
|
### 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`,滚动器目标位置。
|
|||
|
|
- 使用限制:越界时通常被实现类收敛到合法范围,但业务层仍应传递有效索引。
|
|||
|
|
|
|||
|
|
<a id="layout-basic"></a>
|
|||
|
|
### 线性与网格布局
|
|||
|
|
|
|||
|
|
#### 示例 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<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:网格背包
|
|||
|
|
```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<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:空网格的边界结果
|
|||
|
|
```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<DemoSimpleData>(recyclerView);
|
|||
|
|
list.RegisterItemRender<DemoSimpleRender>();
|
|||
|
|
list.Data = new List<DemoSimpleData>(); // 空网格
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="layout-advanced"></a>
|
|||
|
|
### 分页、圆环与异构长度布局
|
|||
|
|
|
|||
|
|
#### 示例 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<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:圆环菜单
|
|||
|
|
```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<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:异构长度列表
|
|||
|
|
```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<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" },
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="scroller"></a>
|
|||
|
|
## 滚动器(Scroller / CircleScroller)
|
|||
|
|
|
|||
|
|
滚动器负责手势输入与位移变化;布局负责“每个位移意味着什么”。二者解耦后,滚动和显示方式可以灵活组合。
|
|||
|
|
|
|||
|
|
<a id="scroller-api"></a>
|
|||
|
|
### 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`,需要上层自行保证。
|
|||
|
|
|
|||
|
|
<a id="scroller-programmatic"></a>
|
|||
|
|
### 程序化滚动
|
|||
|
|
|
|||
|
|
#### 示例 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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="scroller-advanced"></a>
|
|||
|
|
### 惯性、吸附与自动播放
|
|||
|
|
|
|||
|
|
#### 示例 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);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="pool"></a>
|
|||
|
|
## 视图池(ViewProvider / ObjectPool)
|
|||
|
|
|
|||
|
|
视图池是 `RecyclerView` 性能表现的核心:它让“大列表”只保留可见区对象,其他对象进入可复用池而不是销毁重建。
|
|||
|
|
|
|||
|
|
<a id="pool-api"></a>
|
|||
|
|
### 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` | 历史峰值活跃数。 | 可用于回写容量。 |
|
|||
|
|
|
|||
|
|
<a id="pool-basic"></a>
|
|||
|
|
### 预热与复用
|
|||
|
|
|
|||
|
|
#### 示例 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<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>` 自定义容量
|
|||
|
|
```csharp
|
|||
|
|
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:校验失败时自动销毁
|
|||
|
|
```csharp
|
|||
|
|
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 失败后不会重新入池
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="pool-mixed"></a>
|
|||
|
|
### 多模板对象池配置
|
|||
|
|
|
|||
|
|
#### 示例 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<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:根据峰值活跃数回写容量
|
|||
|
|
```csharp
|
|||
|
|
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}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="navigation"></a>
|
|||
|
|
## 导航(RecyclerNavigation)
|
|||
|
|
|
|||
|
|
导航模块负责把 `EventSystem` 的移动事件转成“列表内跳转 + 必要滚动 + 焦点恢复”。
|
|||
|
|
|
|||
|
|
<a id="navigation-api"></a>
|
|||
|
|
### 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` | 无 | 清理交互宿主与状态。 | 回收时自动调用。 |
|
|||
|
|
|
|||
|
|
<a id="navigation-inner"></a>
|
|||
|
|
### 列表内导航
|
|||
|
|
|
|||
|
|
#### 示例 1:ItemRender 启用点击、移动与提交
|
|||
|
|
```csharp
|
|||
|
|
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:主动驱动导航器移动
|
|||
|
|
```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<ViewHolder>();
|
|||
|
|
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<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;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="navigation-entry"></a>
|
|||
|
|
### 列表入口与焦点恢复
|
|||
|
|
|
|||
|
|
#### 示例 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."); // 空列表不强行进入
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="uglist"></a>
|
|||
|
|
## 业务包装层(UGList / UGListExtensions)
|
|||
|
|
|
|||
|
|
`UGList` 家族是官方提供的“业务友好包装层”,适合绝大多数游戏页面。它把适配器创建、内部绑定流程和常见滚动操作做了简化。
|
|||
|
|
|
|||
|
|
<a id="uglist-api"></a>
|
|||
|
|
### 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` | 滚到末端。 | 同上。 |
|
|||
|
|
|
|||
|
|
<a id="uglist-basic"></a>
|
|||
|
|
### 快速创建与滚动扩展
|
|||
|
|
|
|||
|
|
#### 示例 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<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:使用扩展方法滚动
|
|||
|
|
```csharp
|
|||
|
|
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:打开滚动定位调试日志
|
|||
|
|
```csharp
|
|||
|
|
using AlicizaX.UI;
|
|||
|
|
using UnityEngine;
|
|||
|
|
|
|||
|
|
public sealed class UgListDebugDemo : MonoBehaviour
|
|||
|
|
{
|
|||
|
|
private void Awake()
|
|||
|
|
{
|
|||
|
|
UGListExtensions.DebugScrollTo = true; // 打开定位日志
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="uglist-advanced"></a>
|
|||
|
|
### 业务封装示例
|
|||
|
|
|
|||
|
|
#### 示例 1:业务侧封装统一入口
|
|||
|
|
```csharp
|
|||
|
|
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:封装“刷新 + 保留焦点”
|
|||
|
|
```csharp
|
|||
|
|
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); // 数据刷新后恢复焦点
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="scenarios"></a>
|
|||
|
|
## 场景专题
|
|||
|
|
|
|||
|
|
本节给出更贴近业务的完整场景脚本,重点覆盖性能与交互要求较高的页面。
|
|||
|
|
|
|||
|
|
<a id="scenario-massive"></a>
|
|||
|
|
### 大量列表项复用(含对象池配置)
|
|||
|
|
|
|||
|
|
#### 示例 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<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:手工调对象池容量
|
|||
|
|
```csharp
|
|||
|
|
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}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> 💡 大列表调优的首要目标不是“把所有项都放进池里”,而是让“可见项 + 一屏缓冲”命中率足够高。
|
|||
|
|
|
|||
|
|
<a id="scenario-mixed"></a>
|
|||
|
|
### 多模板混排
|
|||
|
|
|
|||
|
|
#### 示例 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<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:未知模板名的保护策略
|
|||
|
|
```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<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`;不要把兜底逻辑留给运行时异常。
|
|||
|
|
|
|||
|
|
<a id="scenario-paging"></a>
|
|||
|
|
### 分页加载(加载态、空态、错误态)
|
|||
|
|
|
|||
|
|
#### 示例 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<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:滚动到底部加载下一页
|
|||
|
|
```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.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="scenario-carousel"></a>
|
|||
|
|
### 轮播与循环滚动(自动播放、手动切换)
|
|||
|
|
|
|||
|
|
#### 示例 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<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:非循环分页的边界保护
|
|||
|
|
```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);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<a id="scenario-nav-scroll"></a>
|
|||
|
|
### 手柄/键盘导航结合列表滚动
|
|||
|
|
|
|||
|
|
#### 示例 1:打开面板自动聚焦第一项
|
|||
|
|
```csharp
|
|||
|
|
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:导航时自动把目标项滚进可见区
|
|||
|
|
```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)` 的意义不只是“滚动”,还包括“滚动结束后恢复焦点”。
|
|||
|
|
|
|||
|
|
<a id="faq"></a>
|
|||
|
|
## 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()`。
|
|||
|
|
- 使用了全量刷新替代差量刷新。
|
|||
|
|
|
|||
|
|
<a id="anti-patterns"></a>
|
|||
|
|
## 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。 |
|
|||
|
|
|
|||
|
|
<a id="performance"></a>
|
|||
|
|
## 性能优化建议
|
|||
|
|
|
|||
|
|
### 建议清单
|
|||
|
|
|
|||
|
|
- 视图池预热:首屏数据尽量一次性设置到位,内部布局流程会自动触发 `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)`,最终会出现错位、重叠或滚动条比例异常。
|
|||
|
|
|
|||
|
|
<a id="checklist"></a>
|
|||
|
|
## 交付前检查清单
|
|||
|
|
|
|||
|
|
- `Templates` 已配置,且多模板名称与 `TemplateName` 完全一致。
|
|||
|
|
- `LayoutManager` 与 `Scroller` 已在 Inspector 上正确绑定。
|
|||
|
|
- 已为所有模板注册对应 `ItemRender`。
|
|||
|
|
- 数据赋值后已通过 `Data` / `SetList()` / `Notify*()` 触发内部布局与刷新流程。
|
|||
|
|
- 差量更新优先使用 `NotifyItemChanged()` / `NotifyItemRangeChanged()`。
|
|||
|
|
- 焦点型页面已验证 `TryFocusIndex()`、`TryFocusEntry()` 与关闭/重开流程。
|
|||
|
|
- 大列表页面已检查 `PoolStats` / `PeakActive` / `GetPeakActiveCount()`。
|
|||
|
|
- 加载态、空态、错误态已验证,且不会因模板缺失报错。
|
|||
|
|
- 循环列表已使用虚拟索引,不直接拿真实索引做连续滚动。
|
|||
|
|
- `OnBind()` 中没有重复注册事件,也没有未释放的临时引用。
|