)
【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库05环境与工具说明项说明代码生成本系列组件库代码由CursorAI 编程助手辅助生成与迭代再结合工程内联调、重构落地Unity 版本2022.3.50f1c1Unity 2022.3 LTSUI 框架UGUIUnityEngine.UI示例工程EleUnityUI仿 Element UI 企业级组件库建议使用Unity 2022.3 LTS同系列版本打开工程。本文实现均在2022.3.50f1c1下验证。上篇04讲完 Table 的选中、列显示与弹窗联动本篇进入独立组件EleTabs。Tabs 标签页图标、下划线指示、内容面板与 Table 内页签的区别Element 的 Tabs 是「页面级分区」上面一排标签下面一整块内容区切换时只显示对应面板。EleUnityUI 的EleTabs按同一心智模型实现并刻意与 Table 顶部的「筛选页签」区分开。本篇覆盖图标— 配置icon键走EleUiIcons下划线指示— 主题色横条跟随当前 Tab 滑动内容面板—ContentHost/Panel_{tabCode}Prefab 里可继续塞 UI与 Table 内页签的区别— 何时用EleTabs何时用EleTable.showTabs涉及文件能力主要文件运行时Runtime/Tabs/EleTabs.cs配置模型Runtime/Tabs/EleTabsModels.csConfig 资产Runtime/Tabs/EleTabsConfigAsset.cs示例生成Editor/Tabs/EleTabsSampleGenerator.cs图标Runtime/Common/EleUiIcons.csTable 内页签对比Runtime/Tables/EleTable.Structure.cs、EleTableModels.cs→EleTableTabConfig颜色继续走设计变量Primary激活文字 / 下划线 / 图标、TextPrimary/TextRegular、White、BorderLighter。一、整体结构先建立全局图EleTabs根 ├── TabBar ← 标签栏固定高度 tabBarHeight │ ├── TabRow ← 横向排布 Tab 按钮 │ │ ├── Tab_user │ │ ├── Tab_config │ │ └── … │ ├── Divider ← 底部分割线 │ └── Underline ← 蓝色下划线指示器 └── ContentHost ← 内容区stretch 填满 TabBar 下方 ├── Panel_user ← 仅当前 Tab 显示 ├── Panel_config └── Panel_role数据流EleTabsConfig.tabs → BuildTabs() 生成按钮 EnsurePanel() → SelectTab(tabCode) → ApplyActiveStyles()文字/图标色 面板显隐 → MoveUnderline()下划线对齐当前按钮 → OnTabChanged(tabCode, tabName)业务侧最小接入tabs.OnTabChanged(code,name){Debug.Log($切换到{name}({code}));};// 往某个面板里动态挂内容varpaneltabs.GetPanel(config);二、配置模型1. 单项EleTabItemConfignewEleTabItemConfig{tabName配置管理,// 显示文字tabCodeconfig,// 唯一编码 → 面板名 Panel_configiconEleUiIcons.Settings,// 图标键空无图标activetrue// 初始选中多项时取第一个 active}2. 整体EleTabsConfigpublicclassEleTabsConfig{publicstringtitleEleTabs;publicEleTableThemethemeEleTableTheme.ElementLight;publicfloattabBarHeight40f;publicfloaticonSize14f;publicfloatunderlineHeight2f;publicboolstretchPanelstrue;publicListEleTabItemConfigtabsnewListEleTabItemConfig();}示例预设EleTabsConfig.CreateSample()包含四个 Tab用户管理 / 配置管理 / 角色管理 / 定时任务补偿分别对应user/config/role/schedule图标键。3. 资源路径资源路径ConfigResources/Tabs/EleTabsSampleConfig.assetPrefabPrefabs/Tabs/EleTabsSample.prefab菜单EleUnityUI → Samples → Tabs → Generate EleTabs Sample EleUnityUI → Samples → Tabs → Spawn EleTabs Preview In Scene GameObject → EleUnityUI → Tabs → Sample三、图标1. 配置方式icon填字符串键由EleUiIcons.Get(icon)取 Sprite。常用键键常量典型用途userEleUiIcons.User用户 / 账号settingsEleUiIcons.Settings配置roleEleUiIcons.Role角色权限clockEleUiIcons.Clock定时 / 任务键不存在或 Sprite 为空时不画图标只显示文字不会报错。2. 实现步骤CreateTabButton里判断item.icon是否为空EleUiIcons.Get成功则创建ImagepreserveAspect true尺寸取config.iconSize默认 14与 Label 横向HLayout间距 6px左右 padding 12激活态icon.color Primary未激活TextRegular3. 关键代码if(!string.IsNullOrEmpty(item.icon)){varspEleUiIcons.Get(item.icon);if(sp!null){iconImgEleUiFactory.CreateImage(Icon,content,EleUiStyle.TextRegular);iconImg.spritesp;iconImg.preserveAspecttrue;floatsMathf.Max(12f,config.iconSize);EleUiFactory.AddFixedColumn(iconImg.gameObject,s,s);}}// ApplyActiveStylesif(t.icon!null)t.icon.coloron?EleUiStyle.Primary:EleUiStyle.TextRegular;扩展新图标在EleUiIcons增加常量 Get()分支 绘制函数即可Tabs 侧不用改。四、下划线指示器Element Tabs 的标志性交互底边主题色横条滑到当前标签下方。EleTabs 用独立Underline节点实现而不是给按钮加 Border避免和 TabBar 底部分割线抢层级。1. 创建BuildTabs末尾varunderImgEleUiFactory.CreateImage(Underline,tabBarRoot,EleUiStyle.Primary);underImg.raycastTargetfalse;underlineunderImg.rectTransform;underline.anchorMinnewVector2(0f,0f);underline.anchorMaxnewVector2(0f,0f);underline.pivotnewVector2(0.5f,0f);underline.sizeDeltanewVector2(48f,Mathf.Max(2f,config.underlineHeight));underline.SetAsLastSibling();// 盖在 Divider 上方underlineHeight默认 2px工业风可略加厚。2. 跟随当前 TabMoveUnderlinevoidMoveUnderline(RectTransformtabBtn){Canvas.ForceUpdateCanvases();LayoutRebuilder.ForceRebuildLayoutImmediate(tabBarRoot);varworldtabBtn.TransformPoint(tabBtn.rect.center);varlocaltabBarRoot.InverseTransformPoint(world);floatwidthMathf.Max(32f,tabBtn.rect.width*0.72f);underline.sizeDeltanewVector2(width,Mathf.Max(2f,config.underlineHeight));underline.anchoredPositionnewVector2(local.x,0f);}要点切换前先ForceRebuildLayoutImmediate避免 HLayout 未算完导致偏移宽度取按钮宽度的72%比整宽略短更接近 Element 视觉pivot在底边中点anchoredPosition.y 0贴在 TabBar 底部3. 激活样式文字boolont.tabCodecurrentTabCode;varcoloron?EleUiStyle.Primary:EleUiStyle.TextPrimary;if(t.label!null)t.label.colorcolor;按钮本身用近乎透明的Button色块 轻微 Primary 悬停高亮主要靠文字、图标和下划线而不是整块背景变色。五、内容面板Panel_{tabCode}1. 命名约定每个 Tab 对应内容区子节点ContentHost/Panel_{tabCode}例如tabCode config→Panel_config。GetPanel(config)就是取这个RectTransform。2.EnsurePanel复用优先RectTransformEnsurePanel(EleTabItemConfigitem){varname$Panel_{item.tabCode};varexistingcontentRoot.Find(name)asRectTransform;if(existing!null){EleUiFactory.Stretch(existing);returnexisting;}varpanelEleUiFactory.CreateImage(name,contentRoot,EleUiStyle.White);EleUiFactory.Stretch(panel.rectTransform);// 首次创建时加占位 LabelPrefab 里可删returnpanel.rectTransform;}已存在的面板不会销毁你在 Prefab 里手动加的子 UITable、表单、图表会在Rebuild后保留。3. 显隐逻辑voidApplyActiveStyles(){foreach(vartin_tabs){boolont.tabCodecurrentTabCode;// …文字 / 图标色…if(t.panel!null)t.panel.gameObject.SetActive(on);}}一次只显示一个Panel_*其余SetActive(false)。不做 Addressables / 懒加载时所有面板在层级里同时存在只是隐藏——简单、适合企业后台体量。4. Prefab 工作流推荐Generate EleTabs Sample生成 Prefab打开Prefabs/Tabs/EleTabsSample.prefab在ContentHost/Panel_config下拖入你的 UI或挂脚本动态创建删掉占位Placeholder文本可选Play 时Start→RebindButtons()保留子节点只重绑点击publicRectTransformGetPanel(stringtabCode){foreach(vartin_tabs)if(t.tabCodetabCode)returnt.panel;returnnull;}代码里往面板塞内容varrttabs.GetPanel(user);vartableInstantiate(tablePrefab,rt);EleUiFactory.Stretch(table.GetComponentRectTransform());5. 重建时清理策略ClearTabBarChildrenKeepPanels只清 TabBar子节点按钮、分割线、下划线会重建ContentHost里配置中仍存在的Panel_{tabCode}保留配置删掉的 Tab 对应面板才销毁这样改tabs列表后点「按 Config 重建 Tabs UI」不会误删你辛苦摆好的面板内容。六、与 Table 内筛选页签的区别这是本篇最容易混淆的一点名字都叫 Tab职责完全不同。1. 对照表维度EleTabs独立组件EleTable.showTabs表内页签定位页面 / 模块级分区同一张表的数据分类筛选配置EleTabsConfig→EleTabItemConfigtabCode/tabName/iconEleTableConfig.tabs→EleTableTabConfigkey/label内容区Panel_{tabCode}整块 UI 切换没有独立面板始终是同一张表切换后果显示/隐藏面板OnTabChanged(code, name)_activeTabKey key→回到第 1 页→RequestCurrentPage()视觉文字 图标 滑动下划线小按钮块TabActive/TabIdle背景色典型场景「用户管理 | 角色管理 | 系统配置」三个子页「全部 | 进行中 | 已归档」筛同一数据源与查询栏无关常与queryBar关键字、状态下拉叠加使用Config 里已写明[Header(页签表内筛选 Tab不是独立 EleTabs)]publicboolshowTabs;publicListEleTableTabConfigtabs;2. Table 内页签实现精简voidBuildTabs(){tabsRoot.gameObject.SetActive(config.showTabsconfig.tabs.Count0);foreach(vartabinconfig.tabs){boolactivetab.key_activeTabKey;varbtnEleUiFactory.CreateEleButton($Tab_{tab.key},tabsRoot,tab.label,...);varkeytab.key;btn.onClick.AddListener((){_activeTabKeykey;BuildTabs();// 刷新按钮高亮OnTabChanged?.Invoke(key);_page1;RequestCurrentPage();// 重新拉表数据});}}业务在OnTabChanged或OnRequestPage里根据ActiveTabKey过滤数据Demo 里也可配合statusFilter。3. 和「筛选」QueryBar 的关系Table 还有一套查询栏showQueryEleTableQueryBar页签 Tab粗粒度分类如业务线、单据状态组查询栏细粒度条件关键字、日期、下拉枚举状态筛选showStatusFilters历史 / 新增等行状态走statusFilter字段三者可并存但都不会创建Panel_*——数据始终绑定到同一个EleTable实例。4. 选型建议需要切换「整块不同 UI」 → EleTabs 各 Panel 里放 Table / 表单 / 图表 需要切换「同一表格不同数据集」 → EleTable.showTabs OnRequestPage 里按 key 过滤 需要临时搜几条 → queryBar不必为此单独做 EleTabs常见组合外层EleTabs分模块某个Panel_*里再放一张EleTable表内页签管该模块下的数据视图。七、API 与生命周期API / 事件说明SetConfig(config, rebuild)换配置并重建Rebuild()/EditorRebuild()编辑器按钮「按 Config 重建 Tabs UI」SelectTab(tabCode, notify)代码切换notifyfalse不抛事件GetPanel(tabCode)取内容面板CurrentTabCode/CurrentTabName当前选中OnTabChanged(tabCode, tabName)生命周期Awake→ApplyThemeStart若未 Build 则Rebuild()若 Prefab 已构建则RebindButtons() 恢复currentTabCode初始 Tab优先currentTabCode序列化值 → 否则第一个activetrue→ 否则列表第一项八、落地检查清单每个tabCode唯一且与Panel_{tabCode}一致图标键能在EleUiIcons.Get里解析切换 Tab 时下划线是否对齐、文字/图标是否变Primary非当前Panel_*是否SetActive(false)Prefab 里手改面板内容后Play 是否仍在RebindButtons路径从 Config 删掉某个 Tab 后对应孤立面板是否被清理不要把「筛表」需求硬塞进EleTabs——应使用EleTable.showTabs验证菜单EleUnityUI → Samples → Tabs → Generate EleTabs Sample EleUnityUI → Samples → Tabs → Spawn EleTabs Preview In Scene九、小结EleTabs 标签栏 下划线 多面板配置驱动Panel_{tabCode}可在 Prefab 扩展图标走EleUiIcons键激活态与下划线同色Primary下划线独立节点 布局刷新后按按钮中心定位宽度约为按钮 72%重建保面板只清 TabBarContentHost 按 tabCode 复用Table 内页签是数据筛选无Panel_*切换会RequestCurrentPage——与EleTabs互补而非替代系列回顾与下篇篇内容03Table 上多行表头、拖宽、排序、分页04Table 下选中、斑马纹、列显示、工具栏与弹窗05Tabs图标、下划线、内容面板、与表内页签区别本文06NavMenu 导航条13 级菜单、maxDepth、顶栏与内容区联动以及和 Tabs 组合成典型后台布局。若你在 UGUI 里做过「Tab 内容被父级 Layout 挤没高度」或「下划线 Play 才偏一格」欢迎留言——ContentHost用手工锚点 stretch、切换前ForceRebuildLayoutImmediate都是为这类坑准备的。