完善反射菜单
This commit is contained in:
+47
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段编组特性
|
||||
/// 将多个字段标记为属于同一个 Entry,共享一个 UI 组件实例
|
||||
///
|
||||
/// 使用示例:
|
||||
/// [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";
|
||||
///
|
||||
/// 上述三个字段将合并为一个 Entry,分别绑定到 UI 组件上 Label、Context、Unit 三个子元素
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class XuiavrgFieldEntryGroupAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
/// <summary>编组路径,以 "/" 分割层级。同一 groupPath 的字段合并到一个 Entry</summary>
|
||||
public readonly string GroupPath;
|
||||
|
||||
/// <summary>该字段在编组 Entry 中的功能标记</summary>
|
||||
public readonly XuiavrgFieldTag Tag;
|
||||
|
||||
/// <summary>同一 Tag 存在多个时的索引区分</summary>
|
||||
public readonly int TagIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 创建字段编组标记
|
||||
/// </summary>
|
||||
/// <param name="groupPath">编组路径 (如 "field1" 或 "field1/child")</param>
|
||||
/// <param name="tag">功能标记</param>
|
||||
/// <param name="tagIndex">标记索引 (同一 Tag 存在多个时区分)</param>
|
||||
public XuiavrgFieldEntryGroupAttribute(string groupPath, XuiavrgFieldTag tag, int tagIndex = 0)
|
||||
{
|
||||
GroupPath = groupPath;
|
||||
Tag = tag;
|
||||
TagIndex = tagIndex;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
// 新的 Entry 体系通过 Manager.BuildEntries 直接处理此 Attribute
|
||||
// 保留 IXuiavrgAttribute 兼容旧的 SchemaHydration 流程
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9de8a9119b60f0642a0fd446d791dc9d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段排序特性
|
||||
/// 控制 Entry 在界面中的显示顺序,替代原有 XuiavrgAttribute.InspectorOrder
|
||||
///
|
||||
/// 使用示例:
|
||||
/// [XuiavrgFieldOrder(5)] public string someField;
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class XuiavrgFieldOrderAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
/// <summary>排序值,值越小越靠前。0 表示使用默认发现顺序</summary>
|
||||
public readonly int Order;
|
||||
|
||||
/// <summary>
|
||||
/// 创建排序标记
|
||||
/// </summary>
|
||||
/// <param name="order">排序值</param>
|
||||
public XuiavrgFieldOrderAttribute(int order)
|
||||
{
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
property.inspectorOrder = Order;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d18234607f666c4ebffb17abcf3d429
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 预制体覆盖特性
|
||||
/// 覆盖对应 Entry 或普通字段生成 UI 时使用的预制体名称
|
||||
///
|
||||
/// 使用示例:
|
||||
/// [XuiavrgFieldPrefabOverride("CustomSpeedPrefab")] public float speed;
|
||||
///
|
||||
/// Manager 在 GenerateUI 阶段会优先使用此名称查找 PropertyGroup
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class XuiavrgFieldPrefabOverrideAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
/// <summary>覆盖的预制体组名称</summary>
|
||||
public readonly string PrefabGroupName;
|
||||
|
||||
/// <summary>
|
||||
/// 创建预制体覆盖标记
|
||||
/// </summary>
|
||||
/// <param name="prefabGroupName">
|
||||
/// 目标预制体组名称,与 XuiavrgPropertyGroupScriptableObject.groupName 匹配
|
||||
/// </param>
|
||||
public XuiavrgFieldPrefabOverrideAttribute(string prefabGroupName)
|
||||
{
|
||||
PrefabGroupName = prefabGroupName;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
// 新的 Entry 体系通过 Manager 直接处理此 Attribute
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46d359a456cc33249aa72c04a95987ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,627 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Pool;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// UI 字段标记提供者接口
|
||||
/// 所有 Tag 增强的 UI 组件需实现此接口,便于 Entry 在绑定阶段自动匹配
|
||||
/// </summary>
|
||||
public interface IXuiavrgFieldTagProvider
|
||||
{
|
||||
XuiavrgFieldTag EntryTag { get; }
|
||||
int TagIndex { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字段 Entry 接口
|
||||
/// 数据对象字段与 UI 组件之间的中间缓存层
|
||||
/// </summary>
|
||||
public interface IFieldEntry
|
||||
{
|
||||
/// <summary>Entry 名称(字段名或编组路径)</summary>
|
||||
string EntryName { get; }
|
||||
|
||||
/// <summary>Entry 完整路径(含父级路径)</summary>
|
||||
string EntryPath { get; }
|
||||
|
||||
/// <summary>主要功能标记</summary>
|
||||
XuiavrgFieldTag Tag { get; }
|
||||
|
||||
/// <summary>标记索引</summary>
|
||||
int TagIndex { get; }
|
||||
|
||||
/// <summary>排序值(越小越靠前)</summary>
|
||||
int FieldOrder { get; set; }
|
||||
|
||||
/// <summary>是否为编组 Entry(包含子 Entry)</summary>
|
||||
bool IsGroupEntry { get; }
|
||||
|
||||
/// <summary>父 Entry</summary>
|
||||
IFieldEntry Parent { get; }
|
||||
|
||||
/// <summary>子 Entry 列表(只读)</summary>
|
||||
IReadOnlyList<IFieldEntry> Children { get; }
|
||||
|
||||
/// <summary>生成的 UI 实例引用</summary>
|
||||
GameObject UIInstance { get; }
|
||||
|
||||
/// <summary>绑定的预制体组</summary>
|
||||
XuiavrgPropertyGroupScriptableObject BoundPrefabGroup { get; set; }
|
||||
|
||||
/// <summary>覆盖的预制体组名称</summary>
|
||||
string PrefabOverrideName { get; set; }
|
||||
|
||||
// === 生命周期 ===
|
||||
|
||||
/// <summary>Entry 创建时调用</summary>
|
||||
void OnEntryCreate();
|
||||
|
||||
/// <summary>绑定 UI 和数据时调用</summary>
|
||||
void OnEntryBind();
|
||||
|
||||
/// <summary>释放时调用(归还池前)</summary>
|
||||
void OnEntryRelease();
|
||||
|
||||
/// <summary>销毁时调用</summary>
|
||||
void OnEntryDestroy();
|
||||
|
||||
// === 数据访问 ===
|
||||
|
||||
/// <summary>获取指定成员的数据值</summary>
|
||||
object GetDataValue(string memberName);
|
||||
|
||||
/// <summary>设置指定成员的数据值</summary>
|
||||
void SetDataValue(string memberName, object value);
|
||||
|
||||
/// <summary>获取 Entry 下所有绑定成员的名称</summary>
|
||||
IEnumerable<string> GetMemberNames();
|
||||
|
||||
// === UI 绑定 ===
|
||||
|
||||
/// <summary>
|
||||
/// 绑定 UI 组件
|
||||
/// </summary>
|
||||
/// <param name="uiInstance">UI 预制体实例</param>
|
||||
/// <param name="componentCache">
|
||||
/// 组件缓存列表。传入 null 时会自动收集 uiInstance 上所有 Component。
|
||||
/// 父 Entry 可将已收集的缓存传递给子 Entry 以提升性能
|
||||
/// </param>
|
||||
void BindUIComponent(GameObject uiInstance, List<Component> componentCache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字段临时 Entry 实现类
|
||||
/// 作为数据对象字段和 UI 组件之间的中间缓存层
|
||||
///
|
||||
/// 支持:
|
||||
/// - 多字段合并为一个 Entry(通过 XuiavrgFieldEntryGroupAttribute)
|
||||
/// - 嵌套 Entry(父 Entry 包含子 Entry)
|
||||
/// - 对象池复用(通过静态 ObjectPool)
|
||||
/// - MVVM 双向绑定的生命周期
|
||||
/// </summary>
|
||||
public class XuiFieldTempEntry : IFieldEntry
|
||||
{
|
||||
#region 对象池
|
||||
|
||||
internal static ObjectPool<XuiFieldTempEntry> Pool { get; private set; }
|
||||
|
||||
static XuiFieldTempEntry()
|
||||
{
|
||||
Pool = new ObjectPool<XuiFieldTempEntry>(
|
||||
createFunc: () => new XuiFieldTempEntry(),
|
||||
actionOnGet: entry => entry.OnEntryCreate(),
|
||||
actionOnRelease: entry => entry.OnEntryRelease(),
|
||||
actionOnDestroy: entry => entry.OnEntryDestroy(),
|
||||
collectionCheck: false,
|
||||
defaultCapacity: 32,
|
||||
maxSize: 512
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>从池中获取 Entry 实例</summary>
|
||||
public static XuiFieldTempEntry Get() => Pool.Get();
|
||||
|
||||
/// <summary>将 Entry 归还池中</summary>
|
||||
public static void Release(XuiFieldTempEntry entry) => Pool.Release(entry);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部绑定数据结构
|
||||
|
||||
/// <summary>
|
||||
/// 成员反射绑定信息
|
||||
/// </summary>
|
||||
internal class MemberBinding
|
||||
{
|
||||
/// <summary>绑定的数据目标对象</summary>
|
||||
public object valueTarget;
|
||||
|
||||
/// <summary>是否为属性(false 为字段)</summary>
|
||||
public bool isProperty;
|
||||
|
||||
/// <summary>属性信息</summary>
|
||||
public PropertyInfo propertyInfo;
|
||||
|
||||
/// <summary>字段信息</summary>
|
||||
public FieldInfo fieldInfo;
|
||||
|
||||
/// <summary>功能标记</summary>
|
||||
public XuiavrgFieldTag tag;
|
||||
|
||||
/// <summary>标记索引</summary>
|
||||
public int tagIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 从绑定目标获取值
|
||||
/// </summary>
|
||||
public object GetValue()
|
||||
{
|
||||
if (valueTarget == null) return null;
|
||||
return isProperty
|
||||
? propertyInfo.GetValue(valueTarget)
|
||||
: fieldInfo.GetValue(valueTarget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置值到绑定目标
|
||||
/// </summary>
|
||||
public void SetValue(object value)
|
||||
{
|
||||
if (valueTarget == null) return;
|
||||
if (isProperty)
|
||||
{
|
||||
if (propertyInfo.CanWrite)
|
||||
propertyInfo.SetValue(valueTarget, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldInfo.SetValue(valueTarget, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字段
|
||||
|
||||
private string _entryName;
|
||||
private string _entryPath;
|
||||
private IFieldEntry _parent;
|
||||
private List<XuiFieldTempEntry> _children = new List<XuiFieldTempEntry>();
|
||||
private Dictionary<string, MemberBinding> _bindings = new Dictionary<string, MemberBinding>();
|
||||
private GameObject _uiInstance;
|
||||
private XuiavrgPropertyGroupScriptableObject _boundPrefabGroup;
|
||||
private string _prefabOverrideName;
|
||||
private int _fieldOrder;
|
||||
|
||||
#endregion
|
||||
|
||||
#region IFieldEntry 属性实现
|
||||
|
||||
public string EntryName => _entryName;
|
||||
|
||||
public string EntryPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_entryPath)) return _entryPath;
|
||||
if (_parent != null)
|
||||
{
|
||||
string parentPath = _parent.EntryPath;
|
||||
return string.IsNullOrEmpty(parentPath)
|
||||
? _entryName
|
||||
: parentPath + "/" + _entryName;
|
||||
}
|
||||
return _entryName;
|
||||
}
|
||||
}
|
||||
|
||||
public XuiavrgFieldTag Tag { get; private set; }
|
||||
public int TagIndex { get; private set; }
|
||||
|
||||
public int FieldOrder
|
||||
{
|
||||
get => _fieldOrder;
|
||||
set => _fieldOrder = value;
|
||||
}
|
||||
|
||||
public bool IsGroupEntry => _children.Count > 0;
|
||||
|
||||
public IFieldEntry Parent => _parent;
|
||||
|
||||
public IReadOnlyList<IFieldEntry> Children => _children;
|
||||
|
||||
public GameObject UIInstance => _uiInstance;
|
||||
|
||||
public XuiavrgPropertyGroupScriptableObject BoundPrefabGroup
|
||||
{
|
||||
get => _boundPrefabGroup;
|
||||
set => _boundPrefabGroup = value;
|
||||
}
|
||||
|
||||
public string PrefabOverrideName
|
||||
{
|
||||
get => _prefabOverrideName;
|
||||
set => _prefabOverrideName = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 初始化方法
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Entry 基本属性
|
||||
/// </summary>
|
||||
public void Initialize(string entryName, XuiavrgFieldTag tag = XuiavrgFieldTag.None, int tagIndex = 0)
|
||||
{
|
||||
_entryName = entryName;
|
||||
_entryPath = null;
|
||||
Tag = tag;
|
||||
TagIndex = tagIndex;
|
||||
_fieldOrder = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置父 Entry(用于嵌套)
|
||||
/// </summary>
|
||||
public void SetParent(IFieldEntry parent)
|
||||
{
|
||||
_parent = parent;
|
||||
_entryPath = null; // 重置路径,下次访问重新计算
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加子 Entry
|
||||
/// </summary>
|
||||
public void AddChild(XuiFieldTempEntry child)
|
||||
{
|
||||
if (child == null) return;
|
||||
child.SetParent(this);
|
||||
_children.Add(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册成员绑定
|
||||
/// </summary>
|
||||
/// <param name="memberName">成员名称</param>
|
||||
/// <param name="memberInfo">反射信息</param>
|
||||
/// <param name="target">数据目标对象</param>
|
||||
/// <param name="tag">功能标记</param>
|
||||
/// <param name="tagIndex">标记索引</param>
|
||||
public void RegisterBinding(string memberName, MemberInfo memberInfo, object target,
|
||||
XuiavrgFieldTag tag = XuiavrgFieldTag.None, int tagIndex = 0)
|
||||
{
|
||||
MemberBinding binding = new MemberBinding
|
||||
{
|
||||
valueTarget = target,
|
||||
tag = tag,
|
||||
tagIndex = tagIndex
|
||||
};
|
||||
|
||||
switch (memberInfo)
|
||||
{
|
||||
case PropertyInfo propInfo:
|
||||
binding.isProperty = true;
|
||||
binding.propertyInfo = propInfo;
|
||||
break;
|
||||
case FieldInfo fieldInfo:
|
||||
binding.isProperty = false;
|
||||
binding.fieldInfo = fieldInfo;
|
||||
break;
|
||||
}
|
||||
|
||||
_bindings[memberName] = binding;
|
||||
|
||||
// 如果是第一个注册的绑定,设置主 Tag
|
||||
if (Tag == XuiavrgFieldTag.None && tag != XuiavrgFieldTag.None)
|
||||
{
|
||||
Tag = tag;
|
||||
TagIndex = tagIndex;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 数据访问
|
||||
|
||||
public object GetDataValue(string memberName)
|
||||
{
|
||||
if (_bindings.TryGetValue(memberName, out var binding))
|
||||
return binding.GetValue();
|
||||
|
||||
// 查找子 Entry
|
||||
foreach (var child in _children)
|
||||
{
|
||||
var value = child.GetDataValue(memberName);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetDataValue(string memberName, object value)
|
||||
{
|
||||
if (_bindings.TryGetValue(memberName, out var binding))
|
||||
{
|
||||
binding.SetValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找子 Entry
|
||||
foreach (var child in _children)
|
||||
{
|
||||
if (child.HasBinding(memberName))
|
||||
{
|
||||
child.SetDataValue(memberName, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetMemberNames() => _bindings.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否包含指定成员绑定
|
||||
/// </summary>
|
||||
public bool HasBinding(string memberName) => _bindings.ContainsKey(memberName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定成员的标记
|
||||
/// </summary>
|
||||
public XuiavrgFieldTag GetMemberTag(string memberName)
|
||||
{
|
||||
if (_bindings.TryGetValue(memberName, out var binding))
|
||||
return binding.tag;
|
||||
return XuiavrgFieldTag.None;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI 绑定
|
||||
|
||||
public void BindUIComponent(GameObject uiInstance, List<Component> componentCache)
|
||||
{
|
||||
_uiInstance = uiInstance;
|
||||
if (_uiInstance == null) return;
|
||||
|
||||
if (componentCache == null)
|
||||
{
|
||||
componentCache = new List<Component>();
|
||||
_uiInstance.GetComponentsInChildren(true, componentCache);
|
||||
}
|
||||
|
||||
BindUIComponentWithCache(_uiInstance, componentCache);
|
||||
|
||||
// 子 Entry 复用组件缓存
|
||||
foreach (var child in _children)
|
||||
{
|
||||
child.BindUIComponent(_uiInstance, componentCache);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindUIComponentWithCache(GameObject uiInstance, List<Component> componentCache)
|
||||
{
|
||||
// 遍历组件缓存,匹配 tag
|
||||
var locators = new List<XuiavrgLocator>();
|
||||
foreach (var comp in componentCache)
|
||||
{
|
||||
if (comp is XuiavrgLocator locator)
|
||||
{
|
||||
locators.Add(locator);
|
||||
}
|
||||
|
||||
if (comp is IXuiavrgFieldTagProvider tagProvider)
|
||||
{
|
||||
// 匹配 Tag 和 TagIndex
|
||||
if (tagProvider.EntryTag == this.Tag &&
|
||||
(this.TagIndex == 0 || tagProvider.TagIndex == this.TagIndex))
|
||||
{
|
||||
// 找到匹配的 UI 组件,建立绑定
|
||||
BuildComponentBinding(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有 IXuiavrgFieldTagProvider,尝试通过 Locator 匹配
|
||||
if (locators.Count > 0)
|
||||
{
|
||||
BuildLocatorBindings(locators);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过标签提供者建立组件绑定
|
||||
/// </summary>
|
||||
private void BuildComponentBinding(Component comp)
|
||||
{
|
||||
// 根据组件类型和标记类型进行数据同步
|
||||
switch (comp)
|
||||
{
|
||||
case UnityEngine.UI.Text uiText when Tag == XuiavrgFieldTag.Label || Tag == XuiavrgFieldTag.Output:
|
||||
uiText.text = GetFirstBindingValue()?.ToString() ?? "";
|
||||
break;
|
||||
case UnityEngine.UI.Button _ when Tag == XuiavrgFieldTag.Button:
|
||||
// 按钮绑定在 BindUIEvents 中处理
|
||||
break;
|
||||
case UnityEngine.UI.Toggle uiToggle when Tag == XuiavrgFieldTag.Toggle:
|
||||
if (GetFirstBindingValue() is bool boolVal)
|
||||
uiToggle.isOn = boolVal;
|
||||
break;
|
||||
case UnityEngine.UI.Slider uiSlider when Tag == XuiavrgFieldTag.Slider:
|
||||
if (GetFirstBindingValue() is float floatVal)
|
||||
uiSlider.value = floatVal;
|
||||
break;
|
||||
case UnityEngine.UI.InputField uiInput when Tag == XuiavrgFieldTag.Input:
|
||||
uiInput.text = GetFirstBindingValue()?.ToString() ?? "";
|
||||
break;
|
||||
case UnityEngine.UI.Dropdown uiDropdown when Tag == XuiavrgFieldTag.Dropdown:
|
||||
if (GetFirstBindingValue() is int intVal)
|
||||
uiDropdown.value = intVal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 Locator 建立绑定
|
||||
/// </summary>
|
||||
private void BuildLocatorBindings(List<XuiavrgLocator> locators)
|
||||
{
|
||||
foreach (var locator in locators)
|
||||
{
|
||||
foreach (var locProp in locator.Properties)
|
||||
{
|
||||
// 按 propertyName 匹配成员名
|
||||
if (_bindings.TryGetValue(locProp.propertyName, out var binding))
|
||||
{
|
||||
// 从数据端同步到 UI
|
||||
var dataValue = binding.GetValue();
|
||||
locProp.SetValue(dataValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取第一个绑定的值
|
||||
/// </summary>
|
||||
private object GetFirstBindingValue()
|
||||
{
|
||||
foreach (var kvp in _bindings)
|
||||
{
|
||||
var val = kvp.Value.GetValue();
|
||||
if (val != null) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件绑定
|
||||
|
||||
/// <summary>
|
||||
/// 绑定 UI 交互事件(双向绑定)
|
||||
/// </summary>
|
||||
public void BindUIEvents()
|
||||
{
|
||||
if (_uiInstance == null) return;
|
||||
|
||||
var allComponents = new List<Component>();
|
||||
_uiInstance.GetComponentsInChildren(true, allComponents);
|
||||
|
||||
foreach (var comp in allComponents)
|
||||
{
|
||||
if (comp is IXuiavrgFieldTagProvider tagProvider)
|
||||
{
|
||||
if (tagProvider.EntryTag != this.Tag) continue;
|
||||
BindComponentEvents(comp);
|
||||
}
|
||||
}
|
||||
|
||||
// 子 Entry 事件绑定
|
||||
foreach (var child in _children)
|
||||
{
|
||||
child.BindUIEvents();
|
||||
}
|
||||
}
|
||||
|
||||
private void BindComponentEvents(Component comp)
|
||||
{
|
||||
switch (comp)
|
||||
{
|
||||
case UnityEngine.UI.Button uiButton:
|
||||
uiButton.onClick.AddListener(() =>
|
||||
{
|
||||
SetFirstBindingValue(true);
|
||||
});
|
||||
break;
|
||||
case UnityEngine.UI.Toggle uiToggle:
|
||||
uiToggle.onValueChanged.AddListener(val =>
|
||||
{
|
||||
SetFirstBindingValue(val);
|
||||
});
|
||||
break;
|
||||
case UnityEngine.UI.Slider uiSlider:
|
||||
uiSlider.onValueChanged.AddListener(val =>
|
||||
{
|
||||
SetFirstBindingValue(val);
|
||||
});
|
||||
break;
|
||||
case UnityEngine.UI.InputField uiInput:
|
||||
uiInput.onEndEdit.AddListener(val =>
|
||||
{
|
||||
SetFirstBindingValue(val);
|
||||
});
|
||||
break;
|
||||
case UnityEngine.UI.Dropdown uiDropdown:
|
||||
uiDropdown.onValueChanged.AddListener(val =>
|
||||
{
|
||||
SetFirstBindingValue(val);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetFirstBindingValue(object value)
|
||||
{
|
||||
foreach (var kvp in _bindings)
|
||||
{
|
||||
kvp.Value.SetValue(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
public void OnEntryCreate()
|
||||
{
|
||||
_entryName = null;
|
||||
_entryPath = null;
|
||||
_parent = null;
|
||||
_fieldOrder = 0;
|
||||
_prefabOverrideName = null;
|
||||
}
|
||||
|
||||
public void OnEntryBind()
|
||||
{
|
||||
BindUIEvents();
|
||||
}
|
||||
|
||||
public void OnEntryRelease()
|
||||
{
|
||||
_entryName = null;
|
||||
_entryPath = null;
|
||||
_parent = null;
|
||||
_uiInstance = null;
|
||||
_boundPrefabGroup = null;
|
||||
_prefabOverrideName = null;
|
||||
Tag = XuiavrgFieldTag.None;
|
||||
TagIndex = 0;
|
||||
_fieldOrder = 0;
|
||||
|
||||
// 清理绑定引用
|
||||
_bindings.Clear();
|
||||
|
||||
// 递归释放子 Entry
|
||||
foreach (var child in _children)
|
||||
{
|
||||
Release(child);
|
||||
}
|
||||
_children.Clear();
|
||||
}
|
||||
|
||||
public void OnEntryDestroy()
|
||||
{
|
||||
_bindings.Clear();
|
||||
_children.Clear();
|
||||
_uiInstance = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f5cdd134ffafe74faa7c37b7b2f9919
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试用枚举 - 用于下拉选择测试
|
||||
/// </summary>
|
||||
public enum XuiavrgTestQuality
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Epic,
|
||||
Legendary
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反射界面生成器测试数据类
|
||||
/// 涵盖新版 Entry 体系的所有特性:编组、排序、预制体覆盖、多种字段类型
|
||||
///
|
||||
/// 使用方式:
|
||||
/// 1. 在 Hierarchy 右键 -> Xeric UI -> Create Reflection UI Example 创建示例节点
|
||||
/// 2. 创建 XuiavrgPropertyGroupScriptableObject 并设置 groupName 和 supportedTags
|
||||
/// 3. 将编组对应的预制体拖入
|
||||
/// 4. 调用 manager.BuildEntries(new XuiavrgExampleTestData()) 构建 Entry
|
||||
/// 5. 调用 manager.GenerateUI() 生成界面
|
||||
/// 或直接使用兼容方法: manager.SchemaHydration(new XuiavrgExampleTestData())
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class XuiavrgExampleTestData
|
||||
{
|
||||
// ==================== 基础数值类型 ====================
|
||||
|
||||
[XuiavrgFieldOrder(1)]
|
||||
[XuiavrgNumberCheck(0, 100)]
|
||||
public int intValue = 42;
|
||||
|
||||
[XuiavrgFieldOrder(2)]
|
||||
[XuiavrgNumberCheck(0f, 999f)]
|
||||
public float floatValue = 3.14f;
|
||||
|
||||
[XuiavrgFieldOrder(3)]
|
||||
public double doubleValue = 123.456;
|
||||
|
||||
[XuiavrgFieldOrder(4)]
|
||||
[XuiavrgNumberCheck(0, 999999)]
|
||||
public long longValue = 100000;
|
||||
|
||||
// ==================== 多字段编组示例 ====================
|
||||
// 以下三个字段合并为一个 Entry,共享一个 UI 组件实例
|
||||
// UI 组件上需要有 Label、Context、Unit 对应的 IXuiavrgFieldTagProvider
|
||||
|
||||
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Label)]
|
||||
[XuiavrgFieldOrder(5)]
|
||||
public string speedLabel = "速度";
|
||||
|
||||
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Context)]
|
||||
[XuiavrgNumberCheck(-999f, 999f)]
|
||||
public float speedValue = 0.1f;
|
||||
|
||||
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Unit)]
|
||||
public string speedUnit = "m/s";
|
||||
|
||||
// 第二个编组示例 - 健康值
|
||||
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Label)]
|
||||
[XuiavrgFieldOrder(6)]
|
||||
public string healthLabel = "生命值";
|
||||
|
||||
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Context)]
|
||||
[XuiavrgNumberCheck(0, 9999)]
|
||||
public float healthValue = 100f;
|
||||
|
||||
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Unit)]
|
||||
public string healthUnit = "HP";
|
||||
|
||||
// ==================== 字符串类型 ====================
|
||||
|
||||
[XuiavrgFieldOrder(7)]
|
||||
[XuiavrgRequired]
|
||||
[XuiavrgStringCheck(minLength: 2, maxLength: 16)]
|
||||
public string playerName = "DefaultName";
|
||||
|
||||
[XuiavrgFieldOrder(8)]
|
||||
[XuiavrgNotEmpty]
|
||||
[XuiavrgStringCheck(onlyEmail: false)]
|
||||
public string emailAddress = "example@mail.com";
|
||||
|
||||
[XuiavrgFieldOrder(9)]
|
||||
[XuiavrgFieldPrefabOverride("MultilineTextArea")]
|
||||
[XuiavrgStringCheck(maxLength: 500)]
|
||||
public string biography = "这是一段个人简介...";
|
||||
|
||||
// ==================== 布尔类型 ====================
|
||||
|
||||
[XuiavrgFieldOrder(10)]
|
||||
public bool enableFeature = true;
|
||||
|
||||
[XuiavrgFieldOrder(11)]
|
||||
public bool advancedMode = false;
|
||||
|
||||
// ==================== 枚举类型 ====================
|
||||
|
||||
[XuiavrgFieldOrder(12)]
|
||||
public XuiavrgTestQuality quality = XuiavrgTestQuality.High;
|
||||
|
||||
// ==================== 隐藏属性 ====================
|
||||
|
||||
[XuiavrgHide(true)]
|
||||
public string hiddenField = "此字段被隐藏,不会生成 Entry";
|
||||
|
||||
// ==================== 不同尺寸 ====================
|
||||
|
||||
[XuiavrgFieldOrder(13)]
|
||||
[XuiavrgAttribute("小号", "size_small", null, size: XuiavrgSize.Small)]
|
||||
public string smallSizeField = "小尺寸";
|
||||
|
||||
[XuiavrgFieldOrder(14)]
|
||||
[XuiavrgAttribute("大号", "size_large", null, size: XuiavrgSize.Large)]
|
||||
public string largeSizeField = "大尺寸";
|
||||
|
||||
// ==================== 集合类型(兼容旧标记) ====================
|
||||
|
||||
[XuiavrgFieldOrder(15)]
|
||||
[XuiavrgCollectionCheck(1, 10, true)]
|
||||
public List<string> tags = new List<string> { "Unity", "C#", "UI" };
|
||||
|
||||
// ==================== 按钮触发器 ====================
|
||||
|
||||
[XuiavrgFieldOrder(16)]
|
||||
public bool submitTrigger = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 211cf8030617ad143ba1f4f68f544658
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 字段 UI 功能标记枚举
|
||||
/// 用于在数据对象字段和 UI 组件之间建立功能映射
|
||||
/// 每个标记代表 UI 组件上的一种功能元素
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum XuiavrgFieldTag
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>标题文本 / Label</summary>
|
||||
Label = 1 << 0,
|
||||
|
||||
/// <summary>内容值 / 数值上下文</summary>
|
||||
Context = 1 << 1,
|
||||
|
||||
/// <summary>单位文本</summary>
|
||||
Unit = 1 << 2,
|
||||
|
||||
/// <summary>布尔开关 / Toggle</summary>
|
||||
Toggle = 1 << 3,
|
||||
|
||||
/// <summary>按钮触发器</summary>
|
||||
Button = 1 << 4,
|
||||
|
||||
/// <summary>滑块 / 进度条</summary>
|
||||
Slider = 1 << 5,
|
||||
|
||||
/// <summary>下拉选择</summary>
|
||||
Dropdown = 1 << 6,
|
||||
|
||||
/// <summary>输入值 / InputField</summary>
|
||||
Input = 1 << 7,
|
||||
|
||||
/// <summary>单选项 / Radio</summary>
|
||||
Select = 1 << 8,
|
||||
|
||||
/// <summary>符号 / 图标</summary>
|
||||
Sign = 1 << 9,
|
||||
|
||||
/// <summary>编辑区域 / 多行文本</summary>
|
||||
Edit = 1 << 10,
|
||||
|
||||
/// <summary>输出区域 / 只读展示</summary>
|
||||
Output = 1 << 11,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3eac1306ca1a5145b8a2cae218062f4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Deconstruction.Type.Peculiarity;
|
||||
#if ODIN_INSPECTOR
|
||||
using Sirenix.OdinInspector;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
using UnityEngine.Pool;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
@@ -15,105 +12,403 @@ namespace XericUI.ReflectionUIGenerator
|
||||
/// 节点下生成反射属性管理器
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 管理器需要挂载在unity节点中,管理器中指定的属性承载节点将用于生成属性内容。
|
||||
/// 管理器需要挂载在 Unity 节点中,管理器中指定的属性承载节点将用于生成属性内容。
|
||||
///
|
||||
/// 为了准确生成界面属性,首先需要准备界面属性预制体。
|
||||
/// 在包含界面交互元素的预制体跟目录上挂载 <see cref="XuiavrgLocator"/> 脚本,其将作为组件入口,管理所有的交互组件,必须在脚本中添加所有允许交互的元素。
|
||||
/// (注:如果你没有odin插件,那么组件样式可能会与设计的不一致,请务必确保每个数组中只包含一个组件)
|
||||
///
|
||||
/// 然后在工程中创建 <see cref="XuiavrPropertyScriptableObject"/> 对象,指定对应的数据类型,将刚刚创建的界面预制体拖入,这样就完成了两者的绑定。
|
||||
/// 为了方便管理,再创建一个 <see cref="XuiavrgPropertyGroupScriptableObject"/> 对象,它可以成为一个组,快速管理和切换所需的数据集合。
|
||||
///
|
||||
/// 接下来,在管理器中填入使用的 <see cref="XuiavrgPropertyGroupScriptableObject"/> 对象,然后调用 SchemaHydration 方法传入负载,界面顷刻生成。
|
||||
/// 新的生命周期流程:
|
||||
/// 1. BuildEntries(object payload) — 传入数据对象,构建 Entry 列表
|
||||
/// 2. OnPostBuildEntries(entries) — 虚方法,可重写添加额外 Entry
|
||||
/// 3. 自动按 FieldOrder 排序
|
||||
/// 4. GenerateUI() — 根据 Entry 列表生成 UI 实例并绑定
|
||||
///
|
||||
/// 对象池说明:
|
||||
/// - Entry 通过 XuiFieldTempEntry.Pool 静态池管理
|
||||
/// - UI 预制体通过内部 PropertyGroupPools 字典管理,以 PropertyGroupScriptableObject 为键
|
||||
/// </remarks>
|
||||
public class XuiavrgInspectorManager : MonoBehaviour
|
||||
{
|
||||
public static Func<XuiavrgInspectorProperty> CreatInspectorProperty = () => new XuiavrgInspectorProperty();
|
||||
public static Action<XuiavrgInspectorProperty> GetInspectorProperty = a => {};
|
||||
public static Action<XuiavrgInspectorProperty> ReleaseInspectorProperty = a => a.Clear();
|
||||
public static Action<XuiavrgInspectorProperty> DestoryInspectorProperty = a => {};
|
||||
public static ObjectPool<XuiavrgInspectorProperty> InspectorPropertyPool
|
||||
= new ObjectPool<XuiavrgInspectorProperty>(CreatInspectorProperty, GetInspectorProperty, ReleaseInspectorProperty, DestoryInspectorProperty);
|
||||
#region 对象池
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
[LabelText("预制体组")]
|
||||
#endif
|
||||
public XuiavrgPropertyGroupScriptableObject propertyUIPrefabsGroup;
|
||||
#if ODIN_INSPECTOR
|
||||
[LabelText("反射属性预览")]
|
||||
#endif
|
||||
public List<XuiavrgInspectorProperty> allProperty = new List<XuiavrgInspectorProperty>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 装填数据负载
|
||||
/// UI 预制体对象池,以 PropertyGroupScriptableObject 为键
|
||||
/// </summary>
|
||||
private Dictionary<XuiavrgPropertyGroupScriptableObject, ObjectPool<GameObject>> _propertyGroupPools
|
||||
= new Dictionary<XuiavrgPropertyGroupScriptableObject, ObjectPool<GameObject>>();
|
||||
|
||||
/// <summary>
|
||||
/// 从指定 PropertyGroup 的预制体池获取 UI 实例
|
||||
/// </summary>
|
||||
public GameObject PropertyGroupPoolGet(XuiavrgPropertyGroupScriptableObject group)
|
||||
{
|
||||
if (group == null) return null;
|
||||
|
||||
if (!_propertyGroupPools.TryGetValue(group, out var pool))
|
||||
{
|
||||
var firstPrefab = group.FirstOrDefault()?.prefab;
|
||||
pool = new ObjectPool<GameObject>(
|
||||
createFunc: () =>
|
||||
firstPrefab != null
|
||||
? Object.Instantiate(firstPrefab, transform)
|
||||
: new GameObject("PropertyItem", typeof(RectTransform)),
|
||||
actionOnGet: go => go.SetActive(true),
|
||||
actionOnRelease: go => go.SetActive(false),
|
||||
actionOnDestroy: go =>
|
||||
{
|
||||
if (go) Destroy(go);
|
||||
},
|
||||
collectionCheck: false,
|
||||
defaultCapacity: 4,
|
||||
maxSize: 64
|
||||
);
|
||||
_propertyGroupPools[group] = pool;
|
||||
}
|
||||
|
||||
return pool.Get();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 UI 实例归还到指定 PropertyGroup 的预制体池
|
||||
/// </summary>
|
||||
public void PropertyGroupPoolRelease(XuiavrgPropertyGroupScriptableObject group, GameObject instance)
|
||||
{
|
||||
if (group == null || instance == null) return;
|
||||
if (_propertyGroupPools.TryGetValue(group, out var pool))
|
||||
{
|
||||
pool.Release(instance);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (instance) Destroy(instance);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有预制体对象池
|
||||
/// </summary>
|
||||
public void ClearAllPropertyGroupPools()
|
||||
{
|
||||
foreach (var pool in _propertyGroupPools.Values)
|
||||
{
|
||||
pool.Clear();
|
||||
}
|
||||
_propertyGroupPools.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字段
|
||||
|
||||
/// <summary>
|
||||
/// 当前活跃的 Entry 列表
|
||||
/// </summary>
|
||||
public List<IFieldEntry> CurrentEntries { get; private set; } = new List<IFieldEntry>();
|
||||
|
||||
/// <summary>
|
||||
/// 当前传入的数据负载
|
||||
/// </summary>
|
||||
public object CurrentPayload { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 核心生命周期
|
||||
|
||||
/// <summary>
|
||||
/// 传入对象实例,构建 Entry 列表
|
||||
/// </summary>
|
||||
/// <param name="payload">数据端对象</param>
|
||||
#if ODIN_INSPECTOR
|
||||
[Button]
|
||||
#endif
|
||||
public void BuildEntries(object payload)
|
||||
{
|
||||
// 清空之前的 Entry
|
||||
ClearCurrentEntries();
|
||||
CurrentPayload = payload;
|
||||
|
||||
if (payload == null)
|
||||
{
|
||||
Debug.LogError("[Xuiavrg] 未指定有效负载,无法构建 Entry");
|
||||
return;
|
||||
}
|
||||
|
||||
var type = payload.GetType();
|
||||
var memberInfos = type.GetRuntimeMemberInfo().ToList();
|
||||
|
||||
// 用于合并编组 Entry 的临时字典
|
||||
var groupEntries = new Dictionary<string, XuiFieldTempEntry>();
|
||||
|
||||
foreach (var memberInfo in memberInfos)
|
||||
{
|
||||
// 检查隐藏标记
|
||||
var hideAttr = memberInfo.GetCustomAttribute<XuiavrgHideAttribute>();
|
||||
if (hideAttr != null && hideAttr.Hide)
|
||||
continue;
|
||||
|
||||
// 检查编组标记
|
||||
var groupAttr = memberInfo.GetCustomAttribute<XuiavrgFieldEntryGroupAttribute>();
|
||||
|
||||
if (groupAttr != null)
|
||||
{
|
||||
// 编组字段:合并到同名 Entry
|
||||
if (!groupEntries.TryGetValue(groupAttr.GroupPath, out var groupEntry))
|
||||
{
|
||||
groupEntry = XuiFieldTempEntry.Get();
|
||||
groupEntry.Initialize(groupAttr.GroupPath);
|
||||
groupEntries[groupAttr.GroupPath] = groupEntry;
|
||||
CurrentEntries.Add(groupEntry);
|
||||
}
|
||||
|
||||
groupEntry.RegisterBinding(
|
||||
memberInfo.Name, memberInfo, payload,
|
||||
groupAttr.Tag, groupAttr.TagIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 普通字段:创建独立 Entry
|
||||
var entry = XuiFieldTempEntry.Get();
|
||||
entry.Initialize(memberInfo.Name);
|
||||
entry.RegisterBinding(memberInfo.Name, memberInfo, payload);
|
||||
|
||||
// 检查预制体覆盖
|
||||
var overrideAttr = memberInfo.GetCustomAttribute<XuiavrgFieldPrefabOverrideAttribute>();
|
||||
if (overrideAttr != null)
|
||||
{
|
||||
entry.PrefabOverrideName = overrideAttr.PrefabGroupName;
|
||||
}
|
||||
|
||||
CurrentEntries.Add(entry);
|
||||
}
|
||||
|
||||
// 应用排序标记
|
||||
var orderAttr = memberInfo.GetCustomAttribute<XuiavrgFieldOrderAttribute>();
|
||||
if (orderAttr != null)
|
||||
{
|
||||
// 找到该成员对应的 Entry(可能是编组 Entry)
|
||||
if (groupAttr != null && groupEntries.TryGetValue(groupAttr.GroupPath, out var gEntry))
|
||||
{
|
||||
if (gEntry.FieldOrder == 0 || orderAttr.Order < gEntry.FieldOrder)
|
||||
gEntry.FieldOrder = orderAttr.Order;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 普通字段的排序应用到其 Entry
|
||||
var normalEntry = CurrentEntries.LastOrDefault();
|
||||
if (normalEntry != null)
|
||||
normalEntry.FieldOrder = orderAttr.Order;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用旧的 IXuiavrgAttribute(兼容)
|
||||
ApplyLegacyAttributes(memberInfo, payload, groupAttr, groupEntries);
|
||||
}
|
||||
|
||||
// 调用后期回调
|
||||
OnPostBuildEntries(CurrentEntries);
|
||||
|
||||
// 排序
|
||||
SortEntries();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用旧的 IXuiavrgAttribute 体系中的属性(兼容保留)
|
||||
/// </summary>
|
||||
private void ApplyLegacyAttributes(
|
||||
MemberInfo memberInfo, object payload,
|
||||
XuiavrgFieldEntryGroupAttribute groupAttr,
|
||||
Dictionary<string, XuiFieldTempEntry> groupEntries)
|
||||
{
|
||||
// 处理旧的 XuiavrgAttribute(设置 labelName, inspectorOrder 等)
|
||||
var oldAttr = memberInfo.GetCustomAttribute<XuiavrgAttribute>();
|
||||
if (oldAttr != null)
|
||||
{
|
||||
// 创建临时的 InspectorProperty 来接收绑定
|
||||
var tempProp = new XuiavrgInspectorProperty();
|
||||
tempProp.RegsiterDataValue(memberInfo, payload);
|
||||
oldAttr.BindProperty(tempProp);
|
||||
|
||||
// 将相关属性同步到 Entry
|
||||
IFieldEntry targetEntry;
|
||||
if (groupAttr != null && groupEntries.TryGetValue(groupAttr.GroupPath, out var gEntry))
|
||||
targetEntry = gEntry;
|
||||
else
|
||||
targetEntry = CurrentEntries.LastOrDefault(e => !e.IsGroupEntry || e == null) ?? CurrentEntries.LastOrDefault();
|
||||
|
||||
if (targetEntry != null)
|
||||
{
|
||||
if (tempProp.inspectorOrder != 0 && targetEntry.FieldOrder == 0)
|
||||
targetEntry.FieldOrder = tempProp.inspectorOrder;
|
||||
|
||||
targetEntry.PrefabOverrideName ??= tempProp.uiPropertyName;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他旧的验证 Attribute
|
||||
foreach (var attr in memberInfo.GetCustomAttributes().OfType<IXuiavrgAttribute>())
|
||||
{
|
||||
if (attr is XuiavrgAttribute) continue; // 已处理
|
||||
if (attr is XuiavrgHideAttribute) continue; // 已处理
|
||||
if (attr is XuiavrgFieldEntryGroupAttribute) continue; // 已处理
|
||||
if (attr is XuiavrgFieldOrderAttribute) continue; // 已处理
|
||||
if (attr is XuiavrgFieldPrefabOverrideAttribute) continue; // 已处理
|
||||
|
||||
// 其他旧 Attribute(Required, StringCheck, NumberCheck 等)
|
||||
var tempProp = new XuiavrgInspectorProperty();
|
||||
tempProp.RegsiterDataValue(memberInfo, payload);
|
||||
attr.BindProperty(tempProp);
|
||||
// 这些验证信息目前由 InspectorProperty 保留,后续可迁移到 Entry
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成后期回调(可重写)
|
||||
/// 在此方法中可以手动添加额外的 Entry(如 Preview 结构、不在数据对象中的内容)
|
||||
/// </summary>
|
||||
/// <param name="entries">当前 Entry 列表</param>
|
||||
protected virtual void OnPostBuildEntries(List<IFieldEntry> entries)
|
||||
{
|
||||
// 默认无操作,子类可重写
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对 Entry 列表按 FieldOrder 排序
|
||||
/// </summary>
|
||||
private void SortEntries()
|
||||
{
|
||||
CurrentEntries.Sort((a, b) =>
|
||||
{
|
||||
int orderCompare = a.FieldOrder.CompareTo(b.FieldOrder);
|
||||
if (orderCompare != 0) return orderCompare;
|
||||
return string.Compare(a.EntryName, b.EntryName, StringComparison.Ordinal);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 Entry 列表生成 UI
|
||||
/// </summary>
|
||||
public void GenerateUI()
|
||||
{
|
||||
if (CurrentEntries == null || CurrentEntries.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[Xuiavrg] 没有可生成的 Entry,请先调用 BuildEntries");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var entry in CurrentEntries)
|
||||
{
|
||||
if (entry is not XuiFieldTempEntry tempEntry) continue;
|
||||
|
||||
// 查找匹配的 PropertyGroup
|
||||
var group = ResolvePropertyGroup(entry);
|
||||
if (group == null)
|
||||
{
|
||||
Debug.LogWarning($"[Xuiavrg] 未找到 Entry '{entry.EntryName}' 的匹配预制体组,跳过");
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.BoundPrefabGroup = group;
|
||||
|
||||
// 从对象池获取 UI 实例
|
||||
var uiInstance = PropertyGroupPoolGet(group);
|
||||
if (uiInstance == null) continue;
|
||||
|
||||
// 设置父节点和命名
|
||||
uiInstance.name = entry.EntryName;
|
||||
uiInstance.transform.SetParent(transform, false);
|
||||
|
||||
// 绑定 UI 组件(首次绑定不传缓存,由 Entry 内部收集)
|
||||
entry.BindUIComponent(uiInstance, null);
|
||||
|
||||
// 绑定事件
|
||||
entry.OnEntryBind();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 Entry 对应的 PropertyGroup
|
||||
/// 优先使用 PrefabOverrideName,否则按 Tag 匹配
|
||||
/// </summary>
|
||||
private XuiavrgPropertyGroupScriptableObject ResolvePropertyGroup(IFieldEntry entry)
|
||||
{
|
||||
// 1. 优先检查 PrefabOverrideName
|
||||
if (!string.IsNullOrEmpty(entry.PrefabOverrideName))
|
||||
{
|
||||
var overrideGroup = XuiavrgPropertyGroupScriptableObject.FindGroupByName(entry.PrefabOverrideName);
|
||||
if (overrideGroup != null) return overrideGroup;
|
||||
}
|
||||
|
||||
// 2. 按 Tag 查找匹配的组
|
||||
if (entry.Tag != XuiavrgFieldTag.None)
|
||||
{
|
||||
var tags = new List<XuiavrgFieldTag> { entry.Tag };
|
||||
// 收集子 Entry 的 Tag(如果是编组 Entry)
|
||||
if (entry.IsGroupEntry)
|
||||
{
|
||||
foreach (var child in entry.Children)
|
||||
{
|
||||
if (child.Tag != XuiavrgFieldTag.None && !tags.Contains(child.Tag))
|
||||
tags.Add(child.Tag);
|
||||
}
|
||||
}
|
||||
|
||||
var matchedGroup = XuiavrgPropertyGroupScriptableObject.FindGroupByTags(tags.ToArray());
|
||||
if (matchedGroup != null) return matchedGroup;
|
||||
}
|
||||
|
||||
// 3. 回退到通用匹配
|
||||
return XuiavrgPropertyGroupScriptableObject.FindGroupByName("Default");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空当前界面和 Entry 列表
|
||||
/// </summary>
|
||||
public void ClearAll()
|
||||
{
|
||||
ClearCurrentEntries();
|
||||
ClearAllPropertyGroupPools();
|
||||
CurrentPayload = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空当前 Entry 列表(归还到池中)
|
||||
/// </summary>
|
||||
private void ClearCurrentEntries()
|
||||
{
|
||||
foreach (var entry in CurrentEntries)
|
||||
{
|
||||
// 释放 UI 实例
|
||||
if (entry.UIInstance != null && entry.BoundPrefabGroup != null)
|
||||
{
|
||||
PropertyGroupPoolRelease(entry.BoundPrefabGroup, entry.UIInstance);
|
||||
}
|
||||
|
||||
// 归还 Entry 到池
|
||||
if (entry is XuiFieldTempEntry tempEntry)
|
||||
{
|
||||
XuiFieldTempEntry.Release(tempEntry);
|
||||
}
|
||||
}
|
||||
CurrentEntries.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ClearAll();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 兼容旧 API
|
||||
|
||||
/// <summary>
|
||||
/// [已弃用] 请使用 BuildEntries + GenerateUI 替代
|
||||
/// </summary>
|
||||
[Obsolete("Use BuildEntries + GenerateUI instead", false)]
|
||||
public void SchemaHydration(object payload)
|
||||
{
|
||||
// 清空环境和界面
|
||||
allProperty.Clear();
|
||||
InspectorPropertyPool.Clear();
|
||||
|
||||
if (!propertyUIPrefabsGroup)
|
||||
{
|
||||
Debug.LogError("未指定编组,无法生成界面");
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload != null)
|
||||
{
|
||||
Debug.LogError("未指定有效负载,无法生成界面");
|
||||
return;
|
||||
}
|
||||
|
||||
// 从反射构建
|
||||
var type = payload.GetType();
|
||||
SchemaHydration(allProperty, type, payload);
|
||||
BuildEntries(payload);
|
||||
GenerateUI();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 状态数据负载
|
||||
/// </summary>
|
||||
/// <param name="properties">反射属性结合</param>
|
||||
/// <param name="payload">负载实例</param>
|
||||
/// <typeparam name="T">进行检查的负载类型</typeparam>
|
||||
private static void SchemaHydration<T>(List<XuiavrgInspectorProperty> properties, object payload)
|
||||
=> SchemaHydration(properties, typeof(T), payload);
|
||||
|
||||
/// <summary>
|
||||
/// 装填数据负载
|
||||
/// </summary>
|
||||
/// <param name="properties">反射属性集合</param>
|
||||
/// <param name="payloadType">进行检查的负载类型</param>
|
||||
/// <param name="payload">负载实例</param>
|
||||
private static void SchemaHydration(List<XuiavrgInspectorProperty> properties, Type payloadType, object payload)
|
||||
{
|
||||
bool needSort = false;
|
||||
foreach (var memberInfo in payloadType.GetRuntimeMemberInfo())
|
||||
{
|
||||
var mw = InspectorPropertyPool.Get();
|
||||
// 绑定负载
|
||||
mw.RegsiterDataValue(memberInfo, payload);
|
||||
// 字段属性上的所有特性都进行标记
|
||||
foreach (var memberAttribute in memberInfo.GetCustomAttributes()
|
||||
.OfType<IXuiavrgAttribute>())
|
||||
{
|
||||
memberAttribute.BindProperty(mw);
|
||||
needSort |= mw.inspectorOrder != 0; // 如果有成员使用了order
|
||||
}
|
||||
properties.Add(mw);
|
||||
}
|
||||
// 排序
|
||||
if (needSort)
|
||||
properties.Sort((property1, property2) => property1.inspectorOrder.CompareTo(property2.inspectorOrder));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+64
-1
@@ -1,19 +1,82 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 界面属性编组
|
||||
/// 提供预制体与 UI 标记的绑定,支持全局静态访问
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[CreateAssetMenu(fileName = "Xeric UI avr Property group", menuName = "Xeric Library/UI Action Vessel/Reflection UI Gen/UI avr Property Group", order = 10)]
|
||||
public class XuiavrgPropertyGroupScriptableObject : ScriptableObject, IList<XuiavrPropertyScriptableObject>
|
||||
{
|
||||
#region 全局注册表
|
||||
|
||||
/// <summary>
|
||||
/// 全局预制体组列表
|
||||
/// 所有 PropertyGroupScriptableObject 在 OnEnable 时自动注册,OnDisable 时自动注销
|
||||
/// </summary>
|
||||
internal static List<XuiavrgPropertyGroupScriptableObject> GlobalPrefabList
|
||||
= new List<XuiavrgPropertyGroupScriptableObject>();
|
||||
|
||||
/// <summary>
|
||||
/// 根据组名查找预制体组
|
||||
/// </summary>
|
||||
public static XuiavrgPropertyGroupScriptableObject FindGroupByName(string groupName)
|
||||
{
|
||||
return GlobalPrefabList.FirstOrDefault(g => g.groupName == groupName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找包含指定标记的预制体组
|
||||
/// </summary>
|
||||
public static List<XuiavrgPropertyGroupScriptableObject> FindGroupsByTag(XuiavrgFieldTag tag)
|
||||
{
|
||||
return GlobalPrefabList.Where(g => g.supportedTags.Contains(tag)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找同时包含所有指定标记的预制体组
|
||||
/// </summary>
|
||||
public static XuiavrgPropertyGroupScriptableObject FindGroupByTags(params XuiavrgFieldTag[] tags)
|
||||
{
|
||||
return GlobalPrefabList.FirstOrDefault(g =>
|
||||
tags.All(t => g.supportedTags.Contains(t)));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字段
|
||||
|
||||
/// <summary>预制体组名称(用于 XuiavrgFieldPrefabOverride 匹配)</summary>
|
||||
public string groupName;
|
||||
|
||||
/// <summary>此组支持的功能标记集合</summary>
|
||||
public List<XuiavrgFieldTag> supportedTags = new List<XuiavrgFieldTag>();
|
||||
|
||||
public List<XuiavrPropertyScriptableObject> propertys = new List<XuiavrPropertyScriptableObject>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (!GlobalPrefabList.Contains(this))
|
||||
GlobalPrefabList.Add(this);
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
GlobalPrefabList.Remove(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public IEnumerator<XuiavrPropertyScriptableObject> GetEnumerator()
|
||||
{
|
||||
return propertys.GetEnumerator();
|
||||
|
||||
Reference in New Issue
Block a user