From 39220c761fdb024eae73a8c3a1bf058347588bb6 Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Fri, 24 Jul 2026 14:51:03 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=8F=8D=E5=B0=84=E7=BB=91?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Runtime/Core/Base/XericUIBehaviour.cs | 43 +++-- .../XuiavrgFieldEntryGroupAttribute.cs | 34 ++-- ...ribute.cs => XuiavrgFieldNameAttribute.cs} | 4 +- ...meta => XuiavrgFieldNameAttribute.cs.meta} | 0 .../Attribute/XuiavrgFieldTagAttribute.cs | 44 +++++ .../Scripts/AvrgMemberMetaData.cs | 17 -- .../Scripts/XuiavrgInspectorManager.cs | 163 +++++++++++------- Runtime/OverrideUI/XuiAvrgMDGroup.cs | 11 +- .../Rendering/Component/XericUIActionTable.cs | 1 + 9 files changed, 195 insertions(+), 122 deletions(-) rename Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/{XuiavrgFieldPrefabOverrideAttribute.cs => XuiavrgFieldNameAttribute.cs} (87%) rename Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/{XuiavrgFieldPrefabOverrideAttribute.cs.meta => XuiavrgFieldNameAttribute.cs.meta} (100%) create mode 100644 Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldTagAttribute.cs diff --git a/Runtime/Core/Base/XericUIBehaviour.cs b/Runtime/Core/Base/XericUIBehaviour.cs index aff53d4..2fd12d0 100644 --- a/Runtime/Core/Base/XericUIBehaviour.cs +++ b/Runtime/Core/Base/XericUIBehaviour.cs @@ -135,13 +135,13 @@ namespace XericUI.Core.Base _graphicRaycaster = null; _hierarchyDirty = true; } - + /// /// 复位层级脏 /// public void ResetHierarchyDirty() { - _hierarchyDirty = false; + _hierarchyDirty = false; rectTransform.SetLayoutDirty(); } @@ -241,21 +241,30 @@ namespace XericUI.Core.Base { var rt = rectTransform; if (rt == null || target == null) return; - var parentRT = rt.parent as RectTransform; - if (parentRT == null) return; - Vector3[] corners = new Vector3[4]; - target.GetWorldCorners(corners); - // corners[0]=左下, corners[1]=左上, corners[2]=右上, corners[3]=右下 - Vector3 worldPos = Vector3.Lerp( - Vector3.Lerp(corners[0], corners[1], anchor.y), - Vector3.Lerp(corners[3], corners[2], anchor.y), - anchor.x); - - if (RectTransformUtility.ScreenPointToLocalPointInRectangle( - parentRT, worldPos, null, out Vector2 localPoint)) + if (rt.parent == target.parent) { - rt.anchoredPosition = localPoint; + rt.anchorMin = anchor; + rt.anchorMax = anchor; + rt.pivot = anchor; + rt.anchoredPosition = Vector2.zero; + return; + } + + var targetWorldPos = target.TransformPoint(new Vector3(anchor.x, anchor.y, 0)); + + // RectTransformUtility 会自动处理父级不一致的情况 + if (RectTransformUtility.ScreenPointToLocalPointInRectangle( + rt.parent as RectTransform, + targetWorldPos, + null, + out Vector2 anchorLocalPos)) + { + Vector2 pivotOffset = new Vector2( + (rt.pivot.x - 0.5f) * rt.rect.width, + (rt.pivot.y - 0.5f) * rt.rect.height + ); + rt.anchoredPosition = anchorLocalPos + pivotOffset; } } @@ -265,9 +274,9 @@ namespace XericUI.Core.Base public virtual void Show() { - gameObject.SetActive(true); + gameObject.SetActive(true); } - + public void Show(bool isShow) { if (isShow) diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldEntryGroupAttribute.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldEntryGroupAttribute.cs index a2a9820..ca9e235 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldEntryGroupAttribute.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldEntryGroupAttribute.cs @@ -4,12 +4,21 @@ namespace XericUI.ReflectionUIGenerator { /// /// 字段编组特性 - /// 将多个字段标记为属于同一个 Entry,共享一个 UI 组件实例 + /// 将多个字段标记为属于同一个编组路径,共享一个 UI 组件实例 /// + /// 编组不携带 Tag,Tag 由 [XuiavrgFieldTag] 单独标记。 /// 使用示例: - /// [XuiavrgFieldEntryGroup("field1", XuiavrgFieldTag.Label)] public string fieldLabel = "速度"; - /// [XuiavrgFieldEntryGroup("field1", XuiavrgFieldTag.Context)] public float fieldValue = 0.1f; - /// [XuiavrgFieldEntryGroup("field1", XuiavrgFieldTag.Unit)] public string fieldUnit = "m/s"; + /// [XuiavrgFieldEntryGroup("field1")] + /// [XuiavrgFieldTag(XuiavrgFieldTag.Label)] + /// public string fieldLabel = "速度"; + /// + /// [XuiavrgFieldEntryGroup("field1")] + /// [XuiavrgFieldTag(XuiavrgFieldTag.Context)] + /// public float fieldValue = 0.1f; + /// + /// [XuiavrgFieldEntryGroup("field1")] + /// [XuiavrgFieldTag(XuiavrgFieldTag.Unit)] + /// public string fieldUnit = "m/s"; /// /// 上述三个字段将合并为一个 Entry,分别绑定到 UI 组件上 Label、Context、Unit 三个子元素 /// @@ -18,31 +27,20 @@ namespace XericUI.ReflectionUIGenerator { /// 编组路径,以 "/" 分割层级。同一 groupPath 的字段合并到一个 Entry public readonly string GroupPath; - - /// 该字段在编组 Entry 中的功能标记 - public readonly XuiavrgFieldTag Tag; - - /// 同一 Tag 存在多个时的索引区分 - public readonly int TagIndex; /// /// 创建字段编组标记 /// - /// 编组路径 (如 "field1" 或 "field1/child") - /// 功能标记 - /// 标记索引 (同一 Tag 存在多个时区分) - public XuiavrgFieldEntryGroupAttribute(string groupPath, XuiavrgFieldTag tag, int tagIndex = 0) + /// 编组路径 (如 "field1" 或 "Stats/Speed") + public XuiavrgFieldEntryGroupAttribute(string groupPath) { GroupPath = groupPath; - Tag = tag; - TagIndex = tagIndex; } public void BindMetaData(AvrgMemberMetaData metaData) { metaData.GroupPath = GroupPath; - metaData.Tag = Tag; - metaData.TagIndex = TagIndex; + // 不设置 Tag/TagIndex,留给 [XuiavrgFieldTag] 单独处理 } } } diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldPrefabOverrideAttribute.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldNameAttribute.cs similarity index 87% rename from Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldPrefabOverrideAttribute.cs rename to Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldNameAttribute.cs index c90bda0..12dc608 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldPrefabOverrideAttribute.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldNameAttribute.cs @@ -12,7 +12,7 @@ namespace XericUI.ReflectionUIGenerator /// Manager 在 GenerateUI 阶段会优先使用此名称查找 PropertyGroup /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] - public class XuiavrgFieldPrefabOverrideAttribute : Attribute, IXuiavrgAttribute + public class XuiavrgFieldNameAttribute : Attribute, IXuiavrgAttribute { /// 覆盖的预制体组名称 public readonly string PrefabGroupName; @@ -23,7 +23,7 @@ namespace XericUI.ReflectionUIGenerator /// /// 目标预制体名称,在 AvrgTypePrefabMappingConfig 中查找匹配 /// - public XuiavrgFieldPrefabOverrideAttribute(string prefabGroupName) + public XuiavrgFieldNameAttribute(string prefabGroupName) { PrefabGroupName = prefabGroupName; } diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldPrefabOverrideAttribute.cs.meta b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldNameAttribute.cs.meta similarity index 100% rename from Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldPrefabOverrideAttribute.cs.meta rename to Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldNameAttribute.cs.meta diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldTagAttribute.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldTagAttribute.cs new file mode 100644 index 0000000..1703083 --- /dev/null +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/Attribute/XuiavrgFieldTagAttribute.cs @@ -0,0 +1,44 @@ +using System; + +namespace XericUI.ReflectionUIGenerator +{ + /// + /// 字段功能标记特性 + /// 单独设置字段的功能标记,与编组分离 + /// + /// 使用示例: + /// [XuiavrgFieldEntryGroup("field1")] + /// [XuiavrgFieldTag(XuiavrgFieldTag.Label)] + /// public string fieldLabel = "速度"; + /// + /// [XuiavrgFieldEntryGroup("field1")] + /// [XuiavrgFieldTag(XuiavrgFieldTag.Context)] + /// public float fieldValue = 0.1f; + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class XuiavrgFieldTagAttribute : Attribute, IXuiavrgAttribute + { + /// 功能标记 + public readonly XuiavrgFieldTag Tag; + + /// 同一 Tag 存在多个时的索引区分 + public readonly int TagIndex; + + /// + /// 创建字段功能标记 + /// + /// 功能标记 + /// 标记索引(同一 Tag 存在多个时区分) + public XuiavrgFieldTagAttribute(XuiavrgFieldTag tag, int tagIndex = 0) + { + Tag = tag; + TagIndex = tagIndex; + } + + public void BindMetaData(AvrgMemberMetaData metaData) + { + metaData.Tag = Tag; + metaData.TagIndex = TagIndex; + } + } +} diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaData.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaData.cs index ad67545..3c0b06b 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaData.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/AvrgMemberMetaData.cs @@ -85,23 +85,6 @@ namespace XericUI.ReflectionUIGenerator /// 是否启用无效值状态检查 public bool ValidateInvalid { get; set; } - // ============ (已弃用字段保留) 旧的验证约束 ============ - - /// 字符串最小长度 - [Obsolete("将移至 ValueRangeConfig 管理")] - public int StringMinLength { get; set; } - - /// 字符串最大长度 - public int StringMaxLength { get; set; } = int.MaxValue; - - /// 数值最小值 - [Obsolete("将移至 ValueRangeConfig 管理")] - public float NumberMinValue { get; set; } - - /// 数值最大值 - [Obsolete("将移至 ValueRangeConfig 管理")] - public float NumberMaxValue { get; set; } - // ============ 原始 Attribute 引用 ============ /// diff --git a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgInspectorManager.cs b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgInspectorManager.cs index a922d06..ccb291d 100644 --- a/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgInspectorManager.cs +++ b/Runtime/InterfaceAttributeReflectionGenerator/Scripts/XuiavrgInspectorManager.cs @@ -7,8 +7,8 @@ using Object = UnityEngine.Object; namespace XericUI.ReflectionUIGenerator { /// - /// 节点下生成反射属性管理器 — Metadata-Driven View Model (MDVM 元数据驱动的视图数据模型) - /// 负责:构建 Entry → 创建组容器 → 生成 UI → 绑定数据 + /// 节点下生成反射属性管理器 — MVM (Metadata-Driven View Model) + /// 负责:构建 Entry → 创建组容器(树状嵌套)→ 生成 UI → 绑定数据 /// public class XuiavrgInspectorManager : MonoBehaviour { @@ -75,9 +75,9 @@ namespace XericUI.ReflectionUIGenerator public AvrgMemberMetaDataSet CurrentMetaDataSet { get; private set; } /// - /// 当前活跃的组容器字典 + /// 编组叶节点字典 — 完整 GroupPath → 对应层级的 XuiAvrgMDGroup /// - private Dictionary _activeGroups = new Dictionary(); + private Dictionary _leafGroupMap = new Dictionary(); [Tooltip("类型-预制体映射配置 ScriptableObject")] public AvrgTypePrefabMappingConfig typePrefabMappingConfig; @@ -170,13 +170,14 @@ namespace XericUI.ReflectionUIGenerator }); } - // ============ 三阶段 GenerateUI ============ + // ============ 四阶段 GenerateUI ============ /// - /// 生成 UI — 三阶段流程 + /// 生成 UI — 四阶段流程 /// Phase A: 按 GroupPath 分组 - /// Phase B: 创建 XuiAvrgMDGroup 组容器 - /// Phase C: 在组容器内生成 UI 组件并绑定 + /// Phase B: 构建路径树,创建嵌套的 XuiAvrgMDGroup 容器 + /// Phase C: 在叶节点容器内生成 UI 组件并绑定 + /// Phase D: 处理非编组 Entry 直接放进根目录 /// public void GenerateUI() { @@ -186,21 +187,19 @@ namespace XericUI.ReflectionUIGenerator return; } - // Phase A: 收集并分组 + // Phase A: 按完整 GroupPath 分组 var groupedEntries = new Dictionary>(); var ungroupedEntries = new List(); foreach (var entry in CurrentEntries) { - string groupKey = !string.IsNullOrEmpty(entry.EntryName) - ? GetGroupPathFromEntry(entry) - : null; + string groupPath = GetGroupPathFromEntry(entry); - if (!string.IsNullOrEmpty(groupKey)) + if (!string.IsNullOrEmpty(groupPath)) { - if (!groupedEntries.ContainsKey(groupKey)) - groupedEntries[groupKey] = new List(); - groupedEntries[groupKey].Add(entry); + if (!groupedEntries.ContainsKey(groupPath)) + groupedEntries[groupPath] = new List(); + groupedEntries[groupPath].Add(entry); } else { @@ -208,18 +207,75 @@ namespace XericUI.ReflectionUIGenerator } } - // Phase B: 创建组容器 - _activeGroups.Clear(); + // Phase B: 构建树状嵌套组容器 + _leafGroupMap.Clear(); + BuildGroupTree(groupedEntries.Keys); + + // Phase C: 在叶节点容器内生成 UI foreach (var kvp in groupedEntries) { string groupPath = kvp.Key; var entries = kvp.Value; - // 检查是否已创建(相同 GroupPath 只创建一个容器) - if (_activeGroups.ContainsKey(groupPath)) continue; + if (!_leafGroupMap.TryGetValue(groupPath, out var leafGroup)) continue; - var groupGo = new GameObject($"Group [{groupPath}]", typeof(RectTransform)); - groupGo.transform.SetParent(transform, false); + foreach (var entry in entries) + { + GenerateSingleUI(entry, leafGroup.contentRoot); + } + } + + // Phase D: 非编组 Entry 直接放进根 + foreach (var entry in ungroupedEntries) + { + GenerateSingleUI(entry, transform); + } + } + + // ============ 树状编组容器构建 ============ + + private void BuildGroupTree(IEnumerable allGroupPaths) + { + var rootNode = new GroupTreeNode(null); + + // Step 1: 将每个完整路径分裂后插入树 + foreach (var fullPath in allGroupPaths) + { + if (string.IsNullOrEmpty(fullPath)) continue; + + var segments = fullPath.Split('/'); + var current = rootNode; + + for (int i = 0; i < segments.Length; i++) + { + string seg = segments[i]; + if (!current.Children.ContainsKey(seg)) + current.Children[seg] = new GroupTreeNode(seg); + current = current.Children[seg]; + } + } + + // Step 2: 深度优先遍历,创建层级 GameObject + CreateGroupGameObjects(rootNode, null, transform, ""); + } + + private void CreateGroupGameObjects( + GroupTreeNode node, + XuiAvrgMDGroup parentGroup, + Transform parentTransform, + string accumulatedPath) + { + foreach (var kvp in node.Children) + { + string segName = kvp.Key; + var childNode = kvp.Value; + + string childFullPath = string.IsNullOrEmpty(accumulatedPath) + ? segName + : accumulatedPath + "/" + segName; + + var groupGo = new GameObject($"Group [{childFullPath}]", typeof(RectTransform)); + groupGo.transform.SetParent(parentTransform, false); var rect = groupGo.GetComponent(); rect.anchorMin = new Vector2(0, 1); @@ -228,35 +284,32 @@ namespace XericUI.ReflectionUIGenerator rect.sizeDelta = new Vector2(0, 0); var group = groupGo.AddComponent(); - int firstOrder = entries.Count > 0 ? entries[0].FieldOrder : 0; - group.Initialize(groupPath, firstOrder); + group.Initialize(childFullPath, 0, parentGroup); - _activeGroups[groupPath] = group; - } - - // Phase C: 生成 UI 组件 - foreach (var kvp in groupedEntries) - { - string groupPath = kvp.Key; - var entries = kvp.Value; - - if (!_activeGroups.TryGetValue(groupPath, out var group)) continue; - - foreach (var entry in entries) + // 叶节点映射到完整路径 + if (childNode.Children.Count == 0) { - GenerateSingleUI(entry, group.contentRoot); + _leafGroupMap[childFullPath] = group; } - } - foreach (var entry in ungroupedEntries) - { - GenerateSingleUI(entry, transform); + CreateGroupGameObjects(childNode, group, group.contentRoot, childFullPath); } } - /// - /// 生成单个 Entry 的 UI - /// + private class GroupTreeNode + { + public string SegmentName { get; } + public Dictionary Children { get; } + + public GroupTreeNode(string name) + { + SegmentName = name; + Children = new Dictionary(); + } + } + + // ============ 生成单个 UI ============ + private void GenerateSingleUI(IFieldEntry entry, Transform parent) { if (entry is not XuiFieldTempEntry tempEntry) return; @@ -276,7 +329,6 @@ namespace XericUI.ReflectionUIGenerator uiInstance.name = entry.EntryName; uiInstance.transform.SetParent(parent, false); - // 检查 XUIAvrgMDValue 是否存在 if (uiInstance.GetComponentInChildren(true) == null) { Debug.LogError($"[Xuiavrg] 预制体 '{prefab.name}' 不符合生成要求:缺少 XUIAvrgMDValue 组件。请确保预制体根节点或其子节点包含 XUIAvrgMDValue 脚本。"); @@ -288,12 +340,8 @@ namespace XericUI.ReflectionUIGenerator entry.OnEntryBind(); } - /// - /// 从 Entry 提取 GroupPath(通过 EntryName 判断) - /// private string GetGroupPathFromEntry(IFieldEntry entry) { - // 通过 Bindings 中的元数据找到 GroupPath if (entry.Bindings != null && entry.Bindings.Count > 0) { var metaData = entry.Bindings[0].MetaData; @@ -355,14 +403,14 @@ namespace XericUI.ReflectionUIGenerator private void ClearGroups() { - foreach (var kvp in _activeGroups) + foreach (var kvp in _leafGroupMap) { if (kvp.Value != null && kvp.Value.gameObject != null) { Destroy(kvp.Value.gameObject); } } - _activeGroups.Clear(); + _leafGroupMap.Clear(); } private void OnDestroy() @@ -370,18 +418,5 @@ namespace XericUI.ReflectionUIGenerator ClearAll(); } - // ============ 兼容旧 API ============ - - [Obsolete("Use BuildFromMetaDataSet + GenerateUI instead", false)] - public void SchemaHydration(object payload) - { - var metaDataSet = XuiavrgUtilities.ExtractAvrgMemberMetaData(payload); - var bindingCollection = metaDataSet.BuildMDVM(payload); - BuildFromMetaDataSet(metaDataSet, bindingCollection); - GenerateUI(); - } - - #endregion - } } diff --git a/Runtime/OverrideUI/XuiAvrgMDGroup.cs b/Runtime/OverrideUI/XuiAvrgMDGroup.cs index af8f5c3..7919946 100644 --- a/Runtime/OverrideUI/XuiAvrgMDGroup.cs +++ b/Runtime/OverrideUI/XuiAvrgMDGroup.cs @@ -21,30 +21,33 @@ namespace XericUI.ReflectionUIGenerator [Tooltip("编组排序值,越小越靠前")] private int _groupOrder; - /// 内容挂载点 — 子 UI 组件应放入此 Transform + /// 内容挂载点 — 子 UI 组件或子组应放入此 Transform public Transform contentRoot; + /// 父编组(为 null 表示根组,直接挂在 Manager 下) + public XuiAvrgMDGroup ParentGroup { get; private set; } + /// 编组路径对应的组名 public string GroupName => _groupName; /// 编组排序值 public int GroupOrder => _groupOrder; - /// 上下文层级深度(如 "group1/subgroup" → depth=2) + /// 上下文层级深度(如 "Stats/Speed" → depth=2) public int ContextDepth { get; private set; } /// /// 初始化编组容器 /// - public void Initialize(string groupPath, int order) + public void Initialize(string groupPath, int order, XuiAvrgMDGroup parent = null) { _groupName = groupPath; _groupOrder = order; + ParentGroup = parent; ContextDepth = string.IsNullOrEmpty(groupPath) ? 0 : groupPath.Split('/').Length; gameObject.name = $"Group [{groupPath}]"; - // 如果没有指定 contentRoot,自动使用自身作为挂载点 if (contentRoot == null) contentRoot = transform; } diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs index 6168114..009fe7f 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.cs @@ -622,6 +622,7 @@ namespace XericUI.XTable.Rendering.Component } /// 设置预制体(ContentType 自动设为 Prefab) + /// 注意是设置预制体而不是实例,设置后订阅单元格预制体生成事件 OnPrefabAcquire 处理获取事件 public XTableCellData SetCellPrefab(int row, int col, GameObject prefab) { var data = TableData.GetOrCreateCell(row, col);