2026-07-10 17:50:20 +08:00
|
|
|
|
# UI 开发指南
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
|
|
|
|
|
## 一、窗口开发
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
窗口类继承 `AbsHFUIWindow`,通过 attribute 声明资源路径和 Canvas 层级:
|
|
|
|
|
|
|
2026-05-22 16:58:41 +08:00
|
|
|
|
```csharp
|
2026-07-10 17:50:20 +08:00
|
|
|
|
[HFAssetBundlePath("UIWindows/ExampleWindow/ExampleWindow.prefab")]
|
|
|
|
|
|
[HFUIWindowLayer(enHFUICanvasLayer.Dynamic)]
|
|
|
|
|
|
public partial class ExampleWindow : AbsHFUIWindow
|
2026-05-22 16:58:41 +08:00
|
|
|
|
{
|
|
|
|
|
|
protected override void OnRunAwake()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnRunAwake();
|
2026-07-10 17:50:20 +08:00
|
|
|
|
// 查找控件、绑定按钮、初始化列表。
|
2026-05-22 16:58:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
protected override void OnOpen(object[] _args)
|
2026-05-22 16:58:41 +08:00
|
|
|
|
{
|
2026-07-10 17:50:20 +08:00
|
|
|
|
base.OnOpen(_args);
|
|
|
|
|
|
// 注册事件并刷新本次显示数据。
|
2026-05-22 16:58:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
protected override void OnClose()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 移除事件和计时器。
|
|
|
|
|
|
base.OnClose();
|
|
|
|
|
|
}
|
2026-05-22 16:58:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
业务打开窗口使用:
|
2026-05-27 16:44:46 +08:00
|
|
|
|
|
2026-05-22 16:58:41 +08:00
|
|
|
|
```csharp
|
2026-07-10 17:50:20 +08:00
|
|
|
|
HFFunCatalogue.UIManager.OpenWindow<ExampleWindow>();
|
2026-05-22 16:58:41 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
不要直接实例化窗口 Prefab,也不要直接调用内部 `HFUIWindowMgr` 单例。
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
## 二、生命周期规则
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
- `OnRunAwake`:实例首次绑定时执行一次,适合控件查找、按钮绑定和虚拟列表初始化。
|
|
|
|
|
|
- `OnOpen`:每次打开执行,适合注册事件、应用参数和刷新数据数量。
|
|
|
|
|
|
- `OnClose`:每次关闭执行,与 `OnOpen` 中的监听、计时器和临时状态对称清理。
|
|
|
|
|
|
- 列表初始化不能依赖数据已经到达;先以数量 `0` 初始化,数据准备后再刷新数量。
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
## 三、单列虚拟列表
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
单列或单行滚动使用 LoopListView2 包装接口:
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
|
|
|
|
|
```csharp
|
2026-07-10 17:50:20 +08:00
|
|
|
|
m_List.SetLoopListViewItemPrefabs(
|
|
|
|
|
|
new GameObject[] { m_ItemTemplate.gameObject }, 0, 1);
|
|
|
|
|
|
|
|
|
|
|
|
m_List.InitListView(
|
|
|
|
|
|
0,
|
|
|
|
|
|
OnGetItem,
|
|
|
|
|
|
OnCreateItem,
|
|
|
|
|
|
OnUpdateItem,
|
|
|
|
|
|
OnGetPrefabIndex,
|
|
|
|
|
|
false);
|
2026-05-22 16:58:41 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
数据准备或变化后:
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
|
|
|
|
|
```csharp
|
2026-07-10 17:50:20 +08:00
|
|
|
|
m_List.RefreshDataTotalCount(m_DataList == null ? 0 : m_DataList.Count);
|
2026-05-22 16:58:41 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
回调职责:
|
2026-05-27 16:44:46 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
| 回调 | 职责 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `OnGetItem` | 从已创建节点取得 `SFUI_ScrollRectItem` |
|
|
|
|
|
|
| `OnCreateItem` | 首次创建绑定类和 SimulateBehaviour |
|
|
|
|
|
|
| `OnUpdateItem` | 根据 dataIndex 更新显示,不能假设旧数据仍有效 |
|
|
|
|
|
|
| `OnGetPrefabIndex` | 多模板时返回模板索引,单模板返回 `0` |
|
2026-05-27 16:44:46 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
垂直列表模板通常使用左上锚点和 pivot `(0, 1)`,宽度与 viewport 匹配;水平列表按滚动方向设置。模板尺寸或 pivot 错误会造成重叠、偏移或不可见。
|
2026-05-27 16:44:46 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
## 四、多列虚拟列表
|
2026-05-27 16:44:46 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
存在固定列数、`GridLayoutGroup` 语义或二维排布时使用 LoopGridView:
|
2026-05-27 16:44:46 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
```csharp
|
|
|
|
|
|
m_Grid.SetLoopGridViewItemPrefabs(
|
|
|
|
|
|
new GameObject[] { m_ItemTemplate.gameObject },
|
|
|
|
|
|
new Vector2(216, 293),
|
|
|
|
|
|
new Vector2(50, 30),
|
|
|
|
|
|
new RectOffset(55, 55, 0, 0),
|
|
|
|
|
|
4,
|
|
|
|
|
|
1);
|
|
|
|
|
|
|
|
|
|
|
|
m_Grid.InitGridView(
|
|
|
|
|
|
0,
|
|
|
|
|
|
OnGetGridItem,
|
|
|
|
|
|
OnCreateGridItem,
|
|
|
|
|
|
OnUpdateGridItem,
|
|
|
|
|
|
OnGetGridPrefabIndex);
|
2026-05-22 16:58:41 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
刷新数量:
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
|
|
|
|
|
```csharp
|
2026-07-10 17:50:20 +08:00
|
|
|
|
m_Grid.RefreshGridDataTotalCount(m_DataList == null ? 0 : m_DataList.Count);
|
2026-05-22 16:58:41 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
不要通过单列 ListView 模拟多列布局。Grid 的 itemSize、spacing、padding 和列数必须与 Prefab 视觉布局一致。
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
## 五、Prefab 设置
|
2026-05-22 16:58:41 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
- 模板节点保持 `SetActive(false)`,但必须位于窗口层级中以便引用收集。
|
|
|
|
|
|
- `SFUI_ScrollRect`、viewport、content 和模板引用必须完整。
|
|
|
|
|
|
- 虚拟列表初始化会关闭旧 `LayoutGroup` 和 `ContentSizeFitter`,运行时布局由 SuperScrollView 管理。
|
|
|
|
|
|
- 不要因为这些旧组件在运行时被关闭就重新勾选或依赖它们计算虚拟列表尺寸。
|
|
|
|
|
|
- 检查模板上的 `LoopListViewItem2` 或 `LoopGridViewItem`;包装层会补组件,但 Prefab 结构仍需正确。
|
|
|
|
|
|
- UIMono 收集需要包含 inactive 子节点,不能只扫描激活节点。
|
2026-05-27 16:29:07 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
## 六、验证清单
|
2026-05-27 16:29:07 +08:00
|
|
|
|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
1. 构建 `SFMono.csproj` 和涉及的 `GameHF.csproj`。
|
|
|
|
|
|
2. 刷新 Unity Assets 并读取 Console。
|
|
|
|
|
|
3. 检查 Prefab 的 viewport、content、模板、RectTransform 和组件启用状态。
|
|
|
|
|
|
4. 验证空数据、1 个元素、超出视口、多次刷新和重复打开。
|
|
|
|
|
|
5. 滚动后验证元素回收与复用,没有 `KeyNotFoundException`、旧数据显示或重复创建。
|
|
|
|
|
|
6. 多列列表验证最后一行不足列数时的尺寸和定位。
|
2026-07-07 18:36:15 +08:00
|
|
|
|
|
|
|
|
|
|
## 文档关联
|
|
|
|
|
|
|
|
|
|
|
|
| 文档 | 关联原因 |
|
|
|
|
|
|
|---|---|
|
2026-07-10 17:50:20 +08:00
|
|
|
|
| [UI 窗口系统](README.md) | 窗口框架入口与生命周期 |
|
|
|
|
|
|
| [界面拼接](界面拼接.md) | Prefab 结构和视觉实现 |
|
|
|
|
|
|
| [UI 拼接工具](../编辑器工具/UI拼接工具.md) | JSON/Prefab 双向转换 |
|
|
|
|
|
|
| [开发规则](../../../rules/20-开发规则.md) | 通用 UI 和 C# 规则 |
|