553 lines
14 KiB
Markdown
553 lines
14 KiB
Markdown
|
|
# 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` 由业务手写
|
|||
|
|
|
|||
|
|
### 示例 2:Widget 使用生成的 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();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例 3:TabWindow
|
|||
|
|
|
|||
|
|
```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>`
|