# UI ## 模块概述 UI 模块负责窗口创建、资源定位、层级管理、显示/关闭、缓存、过渡动画与界面绑定代码协作。 本模块里最容易误解的一点是: - `UIHolderObjectBase`(通常简称 **UIHolder**)不是推荐手写的业务类 - 项目中的大多数 `UIHolder` 都应通过 **UI 绑定工具自动生成** - 业务代码通常只编写 `UIWindow`、`UIWidget`、`UITabWindow` 这类逻辑类 换句话说: - **UIHolder 负责“控件引用和预制体桥接”** - **UI 逻辑类负责“界面行为和生命周期”** ## 快速开始 1. 在场景中挂载 `UIComponent` 2. 为 `UIComponent.uiRoot` 指定 UI 根预制体 3. 在 `UISettingEditorWindow` 中配置 UI 生成规则 4. 选中 UI 预制体根节点,执行 `GameObject/UI生成绑定` 5. 让生成器自动生成 `UIHolder` 类并挂回预制体 6. 在逻辑类中使用 `UIWindow` 或 `UIWidget`,其中 `T` 就是生成出来的 `UIHolder` 类型 ## 架构说明 ```text UIComponent └─ UIService ├─ UIBase ├─ UIWindow ├─ UIWidget ├─ UITabWindow ├─ UIHolderObjectBase ├─ UIMetaRegistry ├─ UIResRegistry └─ UIHolderFactory ``` ### 角色分工 - `UIBase`:UI 逻辑生命周期基类 - `UIWindow`:顶层窗口逻辑 - `UIWidget`:挂在窗口或其他 Widget 下的子部件逻辑 - `UITabWindow`:支持 Tab 页懒加载与切换的窗口基类 - `UIHolderObjectBase`:预制体绑定脚本基类,负责暴露控件引用、`RectTransform`、转场播放器 - `UIHolderFactory`:根据注册信息加载预制体并创建对应 Holder ### `UIBase`、`UIHolderObjectBase`、`UIWidget` 的关系 ```text UI 预制体 └─ 挂载生成的 XXXHolder : UIHolderObjectBase ↑ UIWidget / UIWindow ↑ 通过泛型参数 T 访问 baseui ``` 说明: - `UIHolderObjectBase` 挂在预制体上,持有控件引用 - `UIWindow` / `UIWidget` 的泛型参数 `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` 中如何引用生成的 UIHolder 关键点: - `T` 就是生成工具输出的 Holder 类型 - 逻辑类不需要自己声明控件字段 - 通过 `baseui` 访问自动生成的 Holder 成员 例如: ```csharp using AlicizaX.UI.Runtime; using UnityEngine; public sealed class InventoryItemWidget : UIWidget { 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` 把逻辑和绑定类关联起来 ## 核心类与接口 ### `IUIService` 负责: - 初始化 UI 根节点 - 打开/关闭窗口 - 查询已打开窗口 - 获取层级根节点 - 注入 `ITimerService` ### `UIBase` 关键生命周期: - `OnInitialize()` - `OnOpen()` - `OnClose()` - `OnDestroy()` - `OnUpdate()` 以及对应异步版本: - `OnInitializeAsync()` - `OnOpenAsync()` - `OnCloseAsync()` 并提供: - `CreateWidgetAsync()` - `CreateWidgetSync()` - `RemoveWidget(UIBase widget)` ### `UIWindow` 适合顶层窗口,通常用于: - 主界面 - 设置页 - 背包页 - 弹窗 常用能力: - `CloseSelf()` - 强制关闭 - 打开后顶层排序与层级遮挡处理 ### `UIWidget` 适合子部件,通常用于: - 列表项 - 面板块 - 详情条目 - 页签子页面 公开方法: - `Open(params object[] userDatas)` - `Close()` - `Destroy()` ### `UITabWindow` 用于页签式窗口,支持: - 预注册 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` / `UIWidget` 对应的逻辑实例上 你通常**不会在业务层频繁直接调用它**,因为: - 打开窗口时,`UIService` 会在内部调用 `UIHolderFactory` - 创建 Widget 时,`UIBase.CreateWidgetAsync()` / `CreateWidgetSync()` 也会在内部调用它 可以把它理解为: ```text UI 逻辑类 -> UIService / UIBase -> UIHolderFactory -> 加载预制体 -> 找到生成的 XXXHolder -> 绑定到 UIWindow / UIWidget ``` #### 典型作用场景 1. `ShowUI()` - `UIService` 找到 `InventoryWindow` 对应的元数据 - `UIHolderFactory` 根据 `InventoryWindowHolder` 的 `UIResAttribute` 加载预制体 - 创建并返回 `InventoryWindowHolder` - 把 Holder 绑定给 `InventoryWindow` 2. `CreateWidgetAsync(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(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(previewRoot); if (holder != null) { holder.TextName.text = "Sync Preview"; holder.TextCount.text = "1"; } } } ``` #### 注意事项 - `T` 必须是正确的生成型 `UIHolder`,且继承自 `UIHolderObjectBase` - 对应 Holder 需要已具备 `UIResAttribute`,通常由绑定工具自动生成 - 如果资源路径错误、预制体未挂对应 Holder,`UIHolderFactory` 绑定会失败 - 正常业务打开窗口和创建 Widget 时,优先走 `GameApp.UI.ShowUI()`、`CreateWidgetAsync()`,不建议绕过框架直接大量使用工厂 ## API 参考 ### `IUIService.Initialize(Transform root, bool isOrthographic)` - 必填参数:`root` - 必填参数:`isOrthographic` - 返回值:无 - 说明:初始化 UI 根、Canvas、Camera 与各层级节点 ### `UniTask ShowUI(params object[] userDatas) where T : UIBase` - 可选参数:`userDatas` - 返回值:`UniTask` - 泛型约束:`T : UIBase` - 说明:异步打开窗口 - 推荐:默认优先使用该方法 ### `T ShowUISync(params object[] userDatas) where T : UIBase` - 返回值:`T` - 说明:同步打开窗口 - 注意:仅在资源已就绪时使用 ### `void CloseUI(bool force = false) where T : UIBase` - 可选参数:`force` - 返回值:无 - 说明:关闭指定窗口 ### `CreateWidgetAsync(Transform parent, bool visible = true) where T : UIBase` - 必填参数:`parent` - 可选参数:`visible` - 返回值:`UniTask` - 泛型约束:`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 { protected override async Cysharp.Threading.Tasks.UniTask OnInitializeAsync() { baseui.BtnClose.onClick.AddListener(CloseSelf); InventoryItemWidget item = await CreateWidgetAsync(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 { 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 { protected override void OnInitialize() { InitTabVirtuallyView(baseui.TfTabRoot); InitTabVirtuallyView(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` ## 常见错误 ### 手工编写 UIHolder 导致与生成器冲突 - 现象:字段名、命名空间或资源路径不一致 - 规避:把 Holder 视为生成产物,由工具维护 ### 在 `OnOpen` 中重复注册按钮事件 - 风险:窗口每次打开都会重复绑定 - 正确做法:放到 `OnInitialize` ### 把 `UIWidget` 当顶层窗口直接 `ShowUI` - `UIWidget` 应由父 `UIBase` 通过 `CreateWidgetAsync()` 或 `CreateWidgetSync()` 创建 ## 性能注意事项 - 首次打开大窗口优先预热资源或异步显示 - 使用自动生成 Holder 可以避免大量运行时查找和手工拖引用错误 - 高频销毁/重建的块状内容优先用 `UIWidget`