【Unity UI 进阶】仿 Element UI 打造企业级 Unity UI 组件库(05)
环境与工具说明
| 项 | 说明 |
|---|---|
| 代码生成 | 本系列组件库代码由 Cursor(AI 编程助手)辅助生成与迭代,再结合工程内联调、重构落地 |
| Unity 版本 | 2022.3.50f1c1(Unity 2022.3 LTS) |
| UI 框架 | UGUI(UnityEngine.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.cs |
| Config 资产 | Runtime/Tabs/EleTabsConfigAsset.cs |
| 示例生成 | Editor/Tabs/EleTabsSampleGenerator.cs |
| 图标 | Runtime/Common/EleUiIcons.cs |
| Table 内页签(对比) | Runtime/Tables/EleTable.Structure.cs、EleTableModels.cs → EleTableTabConfig |
颜色继续走 设计变量 :Primary(激活文字 / 下划线 / 图标)、TextPrimary / TextRegular、White、BorderLighter。
一、整体结构(先建立全局图)
text
EleTabs(根)
├── TabBar ← 标签栏(固定高度 tabBarHeight)
│ ├── TabRow ← 横向排布 Tab 按钮
│ │ ├── Tab_user
│ │ ├── Tab_config
│ │ └── ...
│ ├── Divider ← 底部分割线
│ └── Underline ← 蓝色下划线指示器
└── ContentHost ← 内容区(stretch 填满 TabBar 下方)
├── Panel_user ← 仅当前 Tab 显示
├── Panel_config
└── Panel_role
数据流:
text
EleTabsConfig.tabs
→ BuildTabs() 生成按钮 + EnsurePanel()
→ SelectTab(tabCode)
→ ApplyActiveStyles()(文字/图标色 + 面板显隐)
→ MoveUnderline()(下划线对齐当前按钮)
→ OnTabChanged(tabCode, tabName)
业务侧最小接入:
csharp
tabs.OnTabChanged += (code, name) =>
{
Debug.Log($"切换到 {name} ({code})");
};
// 往某个面板里动态挂内容
var panel = tabs.GetPanel("config");
二、配置模型
1. 单项 EleTabItemConfig
csharp
new EleTabItemConfig
{
tabName = "配置管理", // 显示文字
tabCode = "config", // 唯一编码 → 面板名 Panel_config
icon = EleUiIcons.Settings,// 图标键,空=无图标
active = true // 初始选中(多项时取第一个 active)
}
2. 整体 EleTabsConfig
csharp
public class EleTabsConfig
{
public string title = "EleTabs";
public EleTableTheme theme = EleTableTheme.ElementLight;
public float tabBarHeight = 40f;
public float iconSize = 14f;
public float underlineHeight = 2f;
public bool stretchPanels = true;
public List<EleTabItemConfig> tabs = new List<EleTabItemConfig>();
}
示例预设(EleTabsConfig.CreateSample())包含四个 Tab:用户管理 / 配置管理 / 角色管理 / 定时任务补偿,分别对应 user / config / role / schedule 图标键。
3. 资源路径
| 资源 | 路径 |
|---|---|
| Config | Resources/Tabs/EleTabsSampleConfig.asset |
| Prefab | Prefabs/Tabs/EleTabsSample.prefab |
菜单:
text
EleUnityUI → Samples → Tabs → Generate EleTabs Sample
EleUnityUI → Samples → Tabs → Spawn EleTabs Preview In Scene
GameObject → EleUnityUI → Tabs → Sample
三、图标
1. 配置方式
icon 填 字符串键 ,由 EleUiIcons.Get(icon) 取 Sprite。常用键:
| 键 | 常量 | 典型用途 |
|---|---|---|
user |
EleUiIcons.User |
用户 / 账号 |
settings |
EleUiIcons.Settings |
配置 |
role |
EleUiIcons.Role |
角色权限 |
clock |
EleUiIcons.Clock |
定时 / 任务 |
键不存在或 Sprite 为空时,不画图标,只显示文字,不会报错。
2. 实现步骤
CreateTabButton里判断item.icon是否为空EleUiIcons.Get成功则创建Image,preserveAspect = true- 尺寸取
config.iconSize(默认 14) - 与 Label 横向
HLayout,间距 6px,左右 padding 12 - 激活态:
icon.color = Primary;未激活:TextRegular
3. 关键代码
csharp
if (!string.IsNullOrEmpty(item.icon))
{
var sp = EleUiIcons.Get(item.icon);
if (sp != null)
{
iconImg = EleUiFactory.CreateImage("Icon", content, EleUiStyle.TextRegular);
iconImg.sprite = sp;
iconImg.preserveAspect = true;
float s = Mathf.Max(12f, config.iconSize);
EleUiFactory.AddFixedColumn(iconImg.gameObject, s, s);
}
}
// ApplyActiveStyles
if (t.icon != null)
t.icon.color = on ? EleUiStyle.Primary : EleUiStyle.TextRegular;
扩展新图标:在 EleUiIcons 增加常量 + Get() 分支 + 绘制函数即可,Tabs 侧不用改。
四、下划线指示器
Element Tabs 的标志性交互:底边主题色横条 滑到当前标签下方。EleTabs 用独立 Underline 节点实现,而不是给按钮加 Border(避免和 TabBar 底部分割线抢层级)。
1. 创建
BuildTabs 末尾:
csharp
var underImg = EleUiFactory.CreateImage("Underline", tabBarRoot, EleUiStyle.Primary);
underImg.raycastTarget = false;
underline = underImg.rectTransform;
underline.anchorMin = new Vector2(0f, 0f);
underline.anchorMax = new Vector2(0f, 0f);
underline.pivot = new Vector2(0.5f, 0f);
underline.sizeDelta = new Vector2(48f, Mathf.Max(2f, config.underlineHeight));
underline.SetAsLastSibling(); // 盖在 Divider 上方
underlineHeight 默认 2px,工业风可略加厚。
2. 跟随当前 Tab:MoveUnderline
csharp
void MoveUnderline(RectTransform tabBtn)
{
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(tabBarRoot);
var world = tabBtn.TransformPoint(tabBtn.rect.center);
var local = tabBarRoot.InverseTransformPoint(world);
float width = Mathf.Max(32f, tabBtn.rect.width * 0.72f);
underline.sizeDelta = new Vector2(width, Mathf.Max(2f, config.underlineHeight));
underline.anchoredPosition = new Vector2(local.x, 0f);
}
要点:
- 切换前先
ForceRebuildLayoutImmediate,避免 HLayout 未算完导致偏移 - 宽度取按钮宽度的 72%,比整宽略短,更接近 Element 视觉
pivot在底边中点,anchoredPosition.y = 0贴在 TabBar 底部
3. 激活样式(文字)
csharp
bool on = t.tabCode == currentTabCode;
var color = on ? EleUiStyle.Primary : EleUiStyle.TextPrimary;
if (t.label != null) t.label.color = color;
按钮本身用近乎透明的 Button 色块 + 轻微 Primary 悬停,高亮主要靠文字、图标和下划线,而不是整块背景变色。
五、内容面板 Panel_{tabCode}
1. 命名约定
每个 Tab 对应内容区子节点:
text
ContentHost/Panel_{tabCode}
例如 tabCode = "config" → Panel_config。GetPanel("config") 就是取这个 RectTransform。
2. EnsurePanel:复用优先
csharp
RectTransform EnsurePanel(EleTabItemConfig item)
{
var name = $"Panel_{item.tabCode}";
var existing = contentRoot.Find(name) as RectTransform;
if (existing != null)
{
EleUiFactory.Stretch(existing);
return existing;
}
var panel = EleUiFactory.CreateImage(name, contentRoot, EleUiStyle.White);
EleUiFactory.Stretch(panel.rectTransform);
// 首次创建时加占位 Label,Prefab 里可删
return panel.rectTransform;
}
已存在的面板不会销毁 ,你在 Prefab 里手动加的子 UI(Table、表单、图表)会在 Rebuild 后保留。
3. 显隐逻辑
csharp
void ApplyActiveStyles()
{
foreach (var t in _tabs)
{
bool on = t.tabCode == currentTabCode;
// ...文字 / 图标色...
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()保留子节点,只重绑点击
csharp
public RectTransform GetPanel(string tabCode)
{
foreach (var t in _tabs)
if (t.tabCode == tabCode) return t.panel;
return null;
}
代码里往面板塞内容:
csharp
var rt = tabs.GetPanel("user");
var table = Instantiate(tablePrefab, rt);
EleUiFactory.Stretch(table.GetComponent<RectTransform>());
5. 重建时清理策略
ClearTabBarChildrenKeepPanels:
- 只清 TabBar 子节点(按钮、分割线、下划线会重建)
- ContentHost 里:配置中仍存在的
Panel_{tabCode}保留;配置删掉的 Tab 对应面板才销毁
这样改 tabs 列表后点「按 Config 重建 Tabs UI」,不会误删你辛苦摆好的面板内容。
六、与 Table 内筛选页签的区别
这是本篇最容易混淆的一点:名字都叫 Tab,职责完全不同。
1. 对照表
| 维度 | EleTabs(独立组件) |
EleTable.showTabs(表内页签) |
|---|---|---|
| 定位 | 页面 / 模块级分区 | 同一张表的数据 分类筛选 |
| 配置 | EleTabsConfig → EleTabItemConfig(tabCode / tabName / icon) |
EleTableConfig.tabs → EleTableTabConfig(key / label) |
| 内容区 | Panel_{tabCode} 整块 UI 切换 |
没有独立面板,始终是同一张表 |
| 切换后果 | 显示/隐藏面板;OnTabChanged(code, name) |
_activeTabKey = key → 回到第 1 页 → RequestCurrentPage() |
| 视觉 | 文字 + 图标 + 滑动下划线 | 小按钮块,TabActive / TabIdle 背景色 |
| 典型场景 | 「用户管理 | 角色管理 | 系统配置」三个子页 | 「全部 | 进行中 | 已归档」筛同一数据源 |
| 与查询栏 | 无关 | 常与 queryBar(关键字、状态下拉)叠加使用 |
Config 里已写明:
csharp
[Header("页签(表内筛选 Tab,不是独立 EleTabs)")]
public bool showTabs;
public List<EleTableTabConfig> tabs;
2. Table 内页签实现(精简)
csharp
void BuildTabs()
{
tabsRoot.gameObject.SetActive(config.showTabs && config.tabs.Count > 0);
foreach (var tab in config.tabs)
{
bool active = tab.key == _activeTabKey;
var btn = EleUiFactory.CreateEleButton($"Tab_{tab.key}", tabsRoot, tab.label, ...);
var key = tab.key;
btn.onClick.AddListener(() =>
{
_activeTabKey = key;
BuildTabs(); // 刷新按钮高亮
OnTabChanged?.Invoke(key);
_page = 1;
RequestCurrentPage(); // 重新拉表数据
});
}
}
业务在 OnTabChanged 或 OnRequestPage 里根据 ActiveTabKey 过滤数据(Demo 里也可配合 statusFilter)。
3. 和「筛选」QueryBar 的关系
Table 还有一套 查询栏 (showQuery + EleTableQueryBar):
- 页签 Tab:粗粒度分类(如业务线、单据状态组)
- 查询栏:细粒度条件(关键字、日期、下拉枚举)
- 状态筛选
showStatusFilters:历史 / 新增等行状态(走statusFilter字段)
三者可并存,但 都不会 创建 Panel_*------数据始终绑定到同一个 EleTable 实例。
4. 选型建议
text
需要切换「整块不同 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) |
代码切换;notify=false 不抛事件 |
GetPanel(tabCode) |
取内容面板 |
CurrentTabCode / CurrentTabName |
当前选中 |
OnTabChanged |
(tabCode, tabName) |
生命周期:
Awake→ApplyThemeStart:若未 Build 则Rebuild();若 Prefab 已构建则RebindButtons()+ 恢复currentTabCode- 初始 Tab:优先
currentTabCode序列化值 → 否则第一个active=true→ 否则列表第一项
八、落地检查清单
- 每个
tabCode唯一,且与Panel_{tabCode}一致 - 图标键能在
EleUiIcons.Get里解析 - 切换 Tab 时下划线是否对齐、文字/图标是否变
Primary - 非当前
Panel_*是否SetActive(false) - Prefab 里手改面板内容后,Play 是否仍在(
RebindButtons路径) - 从 Config 删掉某个 Tab 后,对应孤立面板是否被清理
- 不要 把「筛表」需求硬塞进
EleTabs------应使用EleTable.showTabs
验证菜单:
text
EleUnityUI → Samples → Tabs → Generate EleTabs Sample
EleUnityUI → Samples → Tabs → Spawn EleTabs Preview In Scene
九、小结
- EleTabs = 标签栏 + 下划线 + 多面板,配置驱动,
Panel_{tabCode}可在 Prefab 扩展 - 图标 走
EleUiIcons键;激活态与下划线同色Primary - 下划线 独立节点 + 布局刷新后按按钮中心定位,宽度约为按钮 72%
- 重建保面板:只清 TabBar,ContentHost 按 tabCode 复用
- Table 内页签 是数据筛选,无
Panel_*,切换会RequestCurrentPage------与EleTabs互补而非替代
系列回顾与下篇
| 篇 | 内容 |
|---|---|
| 03 | Table 上:多行表头、拖宽、排序、分页 |
| 04 | Table 下:选中、斑马纹、列显示、工具栏与弹窗 |
| 05 | Tabs:图标、下划线、内容面板、与表内页签区别(本文) |
06|NavMenu 导航条 :1~3 级菜单、maxDepth、顶栏与内容区联动,以及和 Tabs 组合成典型后台布局。
若你在 UGUI 里做过「Tab 内容被父级 Layout 挤没高度」或「下划线 Play 才偏一格」,欢迎留言------ContentHost 用手工锚点 stretch、切换前 ForceRebuildLayoutImmediate 都是为这类坑准备的。