AlicizaX/Client/Assets/Books/Framework/Runtime/UI.md
2026-04-01 13:20:06 +08:00

553 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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>`