完善反射菜单
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user