AlicizaX/Client/Assets/Books/Framework/Runtime/UI.md

553 lines
14 KiB
Markdown
Raw Normal View History

2026-04-01 13:20:06 +08:00
# UI
## 模块概述
UI 模块负责窗口创建、资源定位、层级管理、显示/关闭、缓存、过渡动画与界面绑定代码协作。
本模块里最容易误解的一点是:
- `UIHolderObjectBase`(通常简称 **UIHolder**)不是推荐手写的业务类
- 项目中的大多数 `UIHolder` 都应通过 **UI 绑定工具自动生成**
- 业务代码通常只编写 `UIWindow<T>`、`UIWidget<T>`、`UITabWindow<T>` 这类逻辑类
换句话说:
- **UIHolder 负责“控件引用和预制体桥接”**
- **UI 逻辑类负责“界面行为和生命周期”**
## 快速开始
1. 在场景中挂载 `UIComponent`
2.`UIComponent.uiRoot` 指定 UI 根预制体
3.`UISettingEditorWindow` 中配置 UI 生成规则
4. 选中 UI 预制体根节点,执行 `GameObject/UI生成绑定`
5. 让生成器自动生成 `UIHolder` 类并挂回预制体
6. 在逻辑类中使用 `UIWindow<T>``UIWidget<T>`,其中 `T` 就是生成出来的 `UIHolder` 类型
## 架构说明
```text
UIComponent
└─ UIService
├─ UIBase
├─ UIWindow<T>
├─ UIWidget<T>
├─ UITabWindow<T>
├─ UIHolderObjectBase
├─ UIMetaRegistry
├─ UIResRegistry
└─ UIHolderFactory
```
### 角色分工
- `UIBase`UI 逻辑生命周期基类
- `UIWindow<T>`:顶层窗口逻辑
- `UIWidget<T>`:挂在窗口或其他 Widget 下的子部件逻辑
- `UITabWindow<T>`:支持 Tab 页懒加载与切换的窗口基类
- `UIHolderObjectBase`:预制体绑定脚本基类,负责暴露控件引用、`RectTransform`、转场播放器
- `UIHolderFactory`:根据注册信息加载预制体并创建对应 Holder
### `UIBase`、`UIHolderObjectBase`、`UIWidget<T>` 的关系
```text
UI 预制体
└─ 挂载生成的 XXXHolder : UIHolderObjectBase
UIWidget<XXXHolder> / UIWindow<XXXHolder>
通过泛型参数 T 访问 baseui
```
说明:
- `UIHolderObjectBase` 挂在预制体上,持有控件引用
- `UIWindow<T>` / `UIWidget<T>` 的泛型参数 `T` 指向该 Holder 类型
- 在逻辑类内部可以通过 `baseui` 访问生成好的控件字段
## UIHolder 的作用
`UIHolderObjectBase` 的职责不是写业务逻辑,而是充当:
- **控件引用容器**:保存按钮、文本、图片、节点等引用
- **预制体桥接层**:让 UI 逻辑层不直接依赖层级查找
- **生命周期事件承载层**:暴露 `OnWindowInitEvent`、`OnWindowAfterShowEvent` 等事件
- **转场入口**:自动寻找并驱动 `IUITransitionPlayer`
因此推荐做法是:
- 业务不手写具体 `XXXHolder`
- 由 UI 绑定工具从预制体结构自动生成
## UIHolder 自动生成工作流
### 1. 配置生成规则
先在编辑器中配置 `UIGenerateConfiguration`,核心配置包括:
- `UIPrefabRootPath`UI 预制体根目录
- `GenerateHolderCodePath`:生成代码输出目录
- `NameSpace`:生成类所在命名空间
- `LoadType``Resources` 或 `AssetBundle`
这些配置通常通过 `UISettingEditorWindow` 维护。
### 2. 按命名规则搭建 UI 预制体
生成器会扫描 UI 节点名和组件类型。例如默认规则会识别:
- `Btn` → 按钮组件
- `Text``TextMeshProUGUI`
- `Img``Image`
- `Tf``Transform`
- `Rect``RectTransform`
例如:
- `Btn#Close@`
- `Text#Title@`
- `Img#Icon@`
生成器会根据这些节点名推断字段名和字段类型。
### 3. 选中 UI 预制体根节点
支持两种常见操作方式:
- 在 Project 中选中 prefab 资源
- 或在 Prefab Mode 中编辑当前预制体
### 4. 执行绑定工具
菜单入口:
- `GameObject/UI生成绑定`
执行后生成器会:
1. 读取当前 UI 生成配置
2. 校验预制体路径是否位于配置的 UI 根目录
3. 扫描可绑定节点
4. 生成 `XXXHolder` 代码文件
5. 脚本编译后自动把生成的 `XXXHolder` 挂到目标预制体根节点
6. 自动回填对应字段引用
这意味着:
- **正常情况下不需要手动创建 Holder 脚本**
- **也不需要手工把字段拖到 Inspector**
### 5. 生成代码的结果
生成的 `UIHolder` 类本质上:
- 继承自 `UIHolderObjectBase`
- 带有 `UIResAttribute`
- 包含自动生成的控件字段
形态类似:
```csharp
using AlicizaX.UI.Runtime;
[UIRes(InventoryItemHolder.ResTag, EUIResLoadType.AssetBundle)]
public class InventoryItemHolder : UIHolderObjectBase
{
public const string ResTag = "UI/Inventory/InventoryItem.prefab";
[UnityEngine.SerializeField] private UnityEngine.UI.Button uiBtnClose;
[UnityEngine.SerializeField] private TMPro.TextMeshProUGUI uiTextTitle;
public UnityEngine.UI.Button BtnClose => uiBtnClose;
public TMPro.TextMeshProUGUI TextTitle => uiTextTitle;
}
```
> 上面是示意结构;实际字段名由生成规则决定。
## 在 `UIWidget<T>` 中如何引用生成的 UIHolder
关键点:
- `T` 就是生成工具输出的 Holder 类型
- 逻辑类不需要自己声明控件字段
- 通过 `baseui` 访问自动生成的 Holder 成员
例如:
```csharp
using AlicizaX.UI.Runtime;
using UnityEngine;
public sealed class InventoryItemWidget : UIWidget<InventoryItemHolder>
{
protected override void OnInitialize()
{
baseui.BtnClose.onClick.AddListener(OnClickClose);
}
protected override void OnOpen()
{
baseui.TextTitle.text = "Potion";
}
private void OnClickClose()
{
Close();
Destroy();
}
}
```
这里的含义是:
- `InventoryItemHolder` 由 UI 绑定工具生成
- `InventoryItemWidget` 是手写业务逻辑
- `UIWidget<InventoryItemHolder>` 把逻辑和绑定类关联起来
## 核心类与接口
### `IUIService`
负责:
- 初始化 UI 根节点
- 打开/关闭窗口
- 查询已打开窗口
- 获取层级根节点
- 注入 `ITimerService`
### `UIBase`
关键生命周期:
- `OnInitialize()`
- `OnOpen()`
- `OnClose()`
- `OnDestroy()`
- `OnUpdate()`
以及对应异步版本:
- `OnInitializeAsync()`
- `OnOpenAsync()`
- `OnCloseAsync()`
并提供:
- `CreateWidgetAsync<T>()`
- `CreateWidgetSync<T>()`
- `RemoveWidget(UIBase widget)`
### `UIWindow<T>`
适合顶层窗口,通常用于:
- 主界面
- 设置页
- 背包页
- 弹窗
常用能力:
- `CloseSelf()`
- 强制关闭
- 打开后顶层排序与层级遮挡处理
### `UIWidget<T>`
适合子部件,通常用于:
- 列表项
- 面板块
- 详情条目
- 页签子页面
公开方法:
- `Open(params object[] userDatas)`
- `Close()`
- `Destroy()`
### `UITabWindow<T>`
用于页签式窗口,支持:
- 预注册 Tab
- 按需懒加载
- `SwitchTab(int index, params object[] userDatas)`
### `UIHolderObjectBase`
核心成员:
- `Target`
- `RectTransform`
- `Visible`
- `OnWindowInitEvent`
- `OnWindowBeforeShowEvent`
- `OnWindowAfterShowEvent`
- `OnWindowBeforeClosedEvent`
- `OnWindowAfterClosedEvent`
- `OnWindowDestroyEvent`
### `UIHolderFactory`
`UIHolderFactory` 是 UI 资源实例化与 Holder 绑定的桥梁,作用是:
- 根据 `UIResRegistry` 中登记的资源信息定位 UI 预制体
- 调用 `IResourceService``Resources` 加载 UI 资源
- 实例化预制体并获取对应的 `UIHolderObjectBase`
- 把生成的 Holder 绑定到 `UIWindow<T>` / `UIWidget<T>` 对应的逻辑实例上
你通常**不会在业务层频繁直接调用它**,因为:
- 打开窗口时,`UIService` 会在内部调用 `UIHolderFactory`
- 创建 Widget 时,`UIBase.CreateWidgetAsync<T>()` / `CreateWidgetSync<T>()` 也会在内部调用它
可以把它理解为:
```text
UI 逻辑类
-> UIService / UIBase
-> UIHolderFactory
-> 加载预制体
-> 找到生成的 XXXHolder
-> 绑定到 UIWindow<T> / UIWidget<T>
```
#### 典型作用场景
1. `ShowUI<InventoryWindow>()`
- `UIService` 找到 `InventoryWindow` 对应的元数据
- `UIHolderFactory` 根据 `InventoryWindowHolder``UIResAttribute` 加载预制体
- 创建并返回 `InventoryWindowHolder`
- 把 Holder 绑定给 `InventoryWindow`
2. `CreateWidgetAsync<InventoryItemWidget>(parent)`
- `UIBase` 创建 `InventoryItemWidget` 的元数据
- `UIHolderFactory` 加载 `InventoryItemHolder` 对应的 Widget 预制体
- 把 Holder 绑定给 `InventoryItemWidget`
#### 直接调用示例
虽然业务层通常不需要直接调用,但在工具代码、调试代码或特殊预加载场景下,可以这样使用:
```csharp
using AlicizaX.UI.Runtime;
using Cysharp.Threading.Tasks;
using UnityEngine;
public sealed class UIHolderFactoryExample : MonoBehaviour
{
[SerializeField] private Transform previewRoot;
private async UniTaskVoid Start()
{
InventoryItemHolder holder = await UIHolderFactory.CreateUIHolderAsync<InventoryItemHolder>(previewRoot);
if (holder != null)
{
holder.TextName.text = "Preview Item";
holder.TextCount.text = "99";
}
}
}
```
同步版本示例:
```csharp
using AlicizaX.UI.Runtime;
using UnityEngine;
public sealed class UIHolderFactorySyncExample : MonoBehaviour
{
[SerializeField] private Transform previewRoot;
private void Start()
{
InventoryItemHolder holder = UIHolderFactory.CreateUIHolderSync<InventoryItemHolder>(previewRoot);
if (holder != null)
{
holder.TextName.text = "Sync Preview";
holder.TextCount.text = "1";
}
}
}
```
#### 注意事项
- `T` 必须是正确的生成型 `UIHolder`,且继承自 `UIHolderObjectBase`
- 对应 Holder 需要已具备 `UIResAttribute`,通常由绑定工具自动生成
- 如果资源路径错误、预制体未挂对应 Holder`UIHolderFactory` 绑定会失败
- 正常业务打开窗口和创建 Widget 时,优先走 `GameApp.UI.ShowUI<T>()`、`CreateWidgetAsync<T>()`,不建议绕过框架直接大量使用工厂
## API 参考
### `IUIService.Initialize(Transform root, bool isOrthographic)`
- 必填参数:`root`
- 必填参数:`isOrthographic`
- 返回值:无
- 说明:初始化 UI 根、Canvas、Camera 与各层级节点
### `UniTask<T> ShowUI<T>(params object[] userDatas) where T : UIBase`
- 可选参数:`userDatas`
- 返回值:`UniTask<T>`
- 泛型约束:`T : UIBase`
- 说明:异步打开窗口
- 推荐:默认优先使用该方法
### `T ShowUISync<T>(params object[] userDatas) where T : UIBase`
- 返回值:`T`
- 说明:同步打开窗口
- 注意:仅在资源已就绪时使用
### `void CloseUI<T>(bool force = false) where T : UIBase`
- 可选参数:`force`
- 返回值:无
- 说明:关闭指定窗口
### `CreateWidgetAsync<T>(Transform parent, bool visible = true) where T : UIBase`
- 必填参数:`parent`
- 可选参数:`visible`
- 返回值:`UniTask<T>`
- 泛型约束:`T : UIBase`
- 说明:从父 UI 创建 Widget
### `RemoveWidget(UIBase widget)`
- 必填参数:`widget`
- 返回值:`UniTask`
- 说明:从父 UI 中移除并销毁 Widget
## 完整使用示例
### 示例 1窗口逻辑 + 自动生成 Holder
```csharp
using AlicizaX.UI.Runtime;
using UnityEngine;
[Window(UILayer.UI, fullScreen: true, cacheTime: 10)]
public sealed class InventoryWindow : UIWindow<InventoryWindowHolder>
{
protected override async Cysharp.Threading.Tasks.UniTask OnInitializeAsync()
{
baseui.BtnClose.onClick.AddListener(CloseSelf);
InventoryItemWidget item = await CreateWidgetAsync<InventoryItemWidget>(baseui.TfContent, false);
item.Open("Potion", 10);
}
protected override void OnOpen()
{
baseui.TextTitle.text = "Inventory";
}
}
```
说明:
- `InventoryWindowHolder` 推荐由 UI 绑定工具生成
- `InventoryWindow` 由业务手写
### 示例 2Widget 使用生成的 Holder
```csharp
using AlicizaX.UI.Runtime;
using UnityEngine;
public sealed class InventoryItemWidget : UIWidget<InventoryItemHolder>
{
private string _itemName;
private int _count;
protected override void OnInitialize()
{
baseui.BtnUse.onClick.AddListener(OnClickUse);
}
protected override void OnOpen()
{
_itemName = (string)UserDatas[0];
_count = (int)UserDatas[1];
baseui.TextName.text = _itemName;
baseui.TextCount.text = _count.ToString();
}
protected override void OnClose()
{
baseui.TextName.text = string.Empty;
baseui.TextCount.text = string.Empty;
}
private void OnClickUse()
{
Debug.Log($"Use item: {_itemName}");
Close();
Destroy();
}
}
```
### 示例 3TabWindow
```csharp
using AlicizaX.UI.Runtime;
[Window(UILayer.UI, fullScreen: true)]
public sealed class SettingWindow : UITabWindow<SettingWindowHolder>
{
protected override void OnInitialize()
{
InitTabVirtuallyView<GraphicsTabWidget>(baseui.TfTabRoot);
InitTabVirtuallyView<AudioTabWidget>(baseui.TfTabRoot);
baseui.BtnGraphics.onClick.AddListener(() => SwitchTab(0));
baseui.BtnAudio.onClick.AddListener(() => SwitchTab(1));
}
protected override void OnOpen()
{
SwitchTab(0);
}
}
```
## 最佳实践
- **不要手写大多数 UIHolder**,优先使用自动生成
- 窗口逻辑类只处理状态和行为,控件引用统一放进 Holder
- `OnInitialize` 做一次性事件绑定,`OnOpen` 做参数刷新
- 默认使用异步打开,避免首帧阻塞
- 列表项和重复块优先拆成 `UIWidget<T>`
## 常见错误
### 手工编写 UIHolder 导致与生成器冲突
- 现象:字段名、命名空间或资源路径不一致
- 规避:把 Holder 视为生成产物,由工具维护
### 在 `OnOpen` 中重复注册按钮事件
- 风险:窗口每次打开都会重复绑定
- 正确做法:放到 `OnInitialize`
### 把 `UIWidget<T>` 当顶层窗口直接 `ShowUI`
- `UIWidget<T>` 应由父 `UIBase` 通过 `CreateWidgetAsync<T>()``CreateWidgetSync<T>()` 创建
## 性能注意事项
- 首次打开大窗口优先预热资源或异步显示
- 使用自动生成 Holder 可以避免大量运行时查找和手工拖引用错误
- 高频销毁/重建的块状内容优先用 `UIWidget<T>`