添加一个表格组件,原理上贴近现代表格系统,理论上可以应对超大型表格以及离散数据集合。
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.UI;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericUI.OverrideUI;
|
||||
|
||||
namespace XericUIEditor.OverrideUI
|
||||
{
|
||||
/// <summary>
|
||||
/// XericStyleButton 的 Inspector 编辑器。
|
||||
/// 完全替换 Selectable 的 Transition/ColorBlock/SpriteState 区域为 StyleSheet 状态块,
|
||||
/// 保留 Interactable、Navigation、onClick 等基础功能。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(XericStyleButton), true)]
|
||||
[CanEditMultipleObjects]
|
||||
internal class XericStyleButtonEditor : SelectableEditor
|
||||
{
|
||||
private SerializedProperty m_InteractableProp;
|
||||
private SerializedProperty m_TargetGraphicProp;
|
||||
private SerializedProperty m_NavigationProp;
|
||||
|
||||
private SerializedProperty m_ColorStateProp;
|
||||
private SerializedProperty m_TextureStateProp;
|
||||
private SerializedProperty m_EnableStyleTransitionProp;
|
||||
private SerializedProperty m_OnClickProp;
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
|
||||
m_InteractableProp = serializedObject.FindProperty("m_Interactable");
|
||||
m_TargetGraphicProp = serializedObject.FindProperty("m_TargetGraphic");
|
||||
m_NavigationProp = serializedObject.FindProperty("m_Navigation");
|
||||
|
||||
m_ColorStateProp = serializedObject.FindProperty("m_ColorState");
|
||||
m_TextureStateProp = serializedObject.FindProperty("m_TextureState");
|
||||
m_EnableStyleTransitionProp = serializedObject.FindProperty("m_EnableStyleTransition");
|
||||
m_OnClickProp = serializedObject.FindProperty("m_OnClick");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
// === 1. Interactable ===
|
||||
EditorGUILayout.PropertyField(m_InteractableProp);
|
||||
|
||||
// === 2. 样式表状态块(替代 Transition + ColorBlock + SpriteState) ===
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("样式表配置(替代 Transition)", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.PropertyField(m_ColorStateProp, new GUIContent("颜色状态块"), true);
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.PropertyField(m_TextureStateProp, new GUIContent("纹理状态块"), true);
|
||||
|
||||
if (m_ColorStateProp.isExpanded || m_TextureStateProp.isExpanded)
|
||||
{
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.PropertyField(m_EnableStyleTransitionProp);
|
||||
}
|
||||
|
||||
// === 3. Target Graphic ===
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.PropertyField(m_TargetGraphicProp);
|
||||
|
||||
Graphic graphic = m_TargetGraphicProp.objectReferenceValue as Graphic;
|
||||
if (graphic == null)
|
||||
graphic = (target as Selectable)?.GetComponent<Graphic>();
|
||||
if (graphic == null)
|
||||
EditorGUILayout.HelpBox("你需要指定一个 Graphic 目标才能使用样式表颜色过渡。", MessageType.Warning);
|
||||
|
||||
// === 4. Navigation ===
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.PropertyField(m_NavigationProp);
|
||||
|
||||
// === 5. onClick ===
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.PropertyField(m_OnClickProp);
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 93a0ee7d0089fb141a33b3db631b645a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,47 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.UI;
|
||||
|
||||
using XericUI.OverrideUI;
|
||||
|
||||
namespace XericUIEditor.OverrideUI
|
||||
{
|
||||
/// <summary>
|
||||
/// XericStyleImage 的 Inspector 编辑器——继承 GraphicEditor,
|
||||
/// 额外展示颜色、纹理、材质三个 StyleField 和自动刷新开关。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(XericStyleImage), true)]
|
||||
[CanEditMultipleObjects]
|
||||
internal class XericStyleImageEditor : GraphicEditor
|
||||
{
|
||||
private SerializedProperty m_ColorStyleProp;
|
||||
private SerializedProperty m_TextureStyleProp;
|
||||
private SerializedProperty m_MaterialStyleProp;
|
||||
private SerializedProperty m_AutoRefreshProp;
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
|
||||
m_ColorStyleProp = serializedObject.FindProperty("m_ColorStyle");
|
||||
m_TextureStyleProp = serializedObject.FindProperty("m_TextureStyle");
|
||||
m_MaterialStyleProp = serializedObject.FindProperty("m_MaterialStyle");
|
||||
m_AutoRefreshProp = serializedObject.FindProperty("m_AutoRefresh");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
base.OnInspectorGUI();
|
||||
EditorGUILayout.Space();
|
||||
|
||||
serializedObject.Update();
|
||||
|
||||
EditorGUILayout.LabelField("样式表配置", EditorStyles.boldLabel);
|
||||
EditorGUILayout.PropertyField(m_ColorStyleProp);
|
||||
EditorGUILayout.PropertyField(m_TextureStyleProp);
|
||||
EditorGUILayout.PropertyField(m_MaterialStyleProp);
|
||||
EditorGUILayout.PropertyField(m_AutoRefreshProp);
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f9db8103c790b3449487b618b5a5db7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,135 @@
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEditor.UI;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericUI.OverrideUI;
|
||||
|
||||
namespace XericUIEditor.OverrideUI
|
||||
{
|
||||
/// <summary>
|
||||
/// XericStyleToggle 的 Inspector 编辑器。
|
||||
/// 完全替换 Selectable 的 Transition/ColorBlock/SpriteState 区域为 StyleSheet 状态块,
|
||||
/// 保留 Interactable、Navigation、isOn、graphic、group、onValueChanged 等基础功能。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(XericStyleToggle), true)]
|
||||
[CanEditMultipleObjects]
|
||||
internal class XericStyleToggleEditor : SelectableEditor
|
||||
{
|
||||
// Selectable 基础
|
||||
private SerializedProperty m_InteractableProp;
|
||||
private SerializedProperty m_NavigationProp;
|
||||
|
||||
// Toggle 基础
|
||||
private SerializedProperty m_IsOnProp;
|
||||
private SerializedProperty m_TransitionProp;
|
||||
private SerializedProperty m_GraphicProp;
|
||||
private SerializedProperty m_GroupProp;
|
||||
private SerializedProperty m_OnValueChangedProp;
|
||||
|
||||
// StyleSheet 专用
|
||||
private SerializedProperty m_ColorStateProp;
|
||||
private SerializedProperty m_TextureStateProp;
|
||||
private SerializedProperty m_OnOverrideColorProp;
|
||||
private SerializedProperty m_OnOverrideTextureProp;
|
||||
private SerializedProperty m_CheckmarkStyleProp;
|
||||
private SerializedProperty m_EnableStyleTransitionProp;
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
|
||||
m_InteractableProp = serializedObject.FindProperty("m_Interactable");
|
||||
m_NavigationProp = serializedObject.FindProperty("m_Navigation");
|
||||
|
||||
m_IsOnProp = serializedObject.FindProperty("m_IsOn");
|
||||
m_TransitionProp = serializedObject.FindProperty("toggleTransition");
|
||||
m_GraphicProp = serializedObject.FindProperty("graphic");
|
||||
m_GroupProp = serializedObject.FindProperty("m_Group");
|
||||
m_OnValueChangedProp = serializedObject.FindProperty("onValueChanged");
|
||||
|
||||
m_ColorStateProp = serializedObject.FindProperty("m_ColorState");
|
||||
m_TextureStateProp = serializedObject.FindProperty("m_TextureState");
|
||||
m_OnOverrideColorProp = serializedObject.FindProperty("m_OnOverrideColor");
|
||||
m_OnOverrideTextureProp = serializedObject.FindProperty("m_OnOverrideTexture");
|
||||
m_CheckmarkStyleProp = serializedObject.FindProperty("m_CheckmarkStyle");
|
||||
m_EnableStyleTransitionProp = serializedObject.FindProperty("m_EnableStyleTransition");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
XericStyleToggle toggle = target as XericStyleToggle;
|
||||
|
||||
// === 1. Interactable ===
|
||||
EditorGUILayout.PropertyField(m_InteractableProp);
|
||||
|
||||
// === 2. 样式表状态块(替代 Transition + ColorBlock + SpriteState) ===
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.LabelField("样式表配置(替代 Transition)", EditorStyles.boldLabel);
|
||||
|
||||
EditorGUILayout.PropertyField(m_ColorStateProp, new GUIContent("颜色状态块"), true);
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.PropertyField(m_TextureStateProp, new GUIContent("纹理状态块"), true);
|
||||
|
||||
bool anyExpanded = m_ColorStateProp.isExpanded || m_TextureStateProp.isExpanded;
|
||||
if (anyExpanded)
|
||||
{
|
||||
EditorGUILayout.Space(2);
|
||||
EditorGUILayout.PropertyField(m_OnOverrideColorProp);
|
||||
EditorGUILayout.PropertyField(m_OnOverrideTextureProp);
|
||||
EditorGUILayout.PropertyField(m_CheckmarkStyleProp);
|
||||
EditorGUILayout.PropertyField(m_EnableStyleTransitionProp);
|
||||
}
|
||||
|
||||
// === 3. isOn ===
|
||||
EditorGUILayout.Space();
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUILayout.PropertyField(m_IsOnProp);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
EditorSceneManager.MarkSceneDirty(toggle!.gameObject.scene);
|
||||
|
||||
ToggleGroup group = m_GroupProp.objectReferenceValue as ToggleGroup;
|
||||
toggle!.isOn = m_IsOnProp.boolValue;
|
||||
|
||||
if (group != null && group.isActiveAndEnabled && toggle!.IsActive())
|
||||
{
|
||||
if (toggle!.isOn || (!group.AnyTogglesOn() && !group.allowSwitchOff))
|
||||
{
|
||||
toggle!.isOn = true;
|
||||
group.NotifyToggleOn(toggle!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === 4. Toggle 切换效果 & Graphic ===
|
||||
EditorGUILayout.PropertyField(m_TransitionProp);
|
||||
EditorGUILayout.PropertyField(m_GraphicProp);
|
||||
|
||||
// === 5. Group ===
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUILayout.PropertyField(m_GroupProp);
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
EditorSceneManager.MarkSceneDirty(toggle!.gameObject.scene);
|
||||
toggle!.group = m_GroupProp.objectReferenceValue as ToggleGroup;
|
||||
}
|
||||
|
||||
// === 6. Navigation ===
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.PropertyField(m_NavigationProp);
|
||||
|
||||
// === 7. onValueChanged ===
|
||||
EditorGUILayout.Space();
|
||||
EditorGUILayout.PropertyField(m_OnValueChangedProp);
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 333577a929d25434e87d0a53360fce29
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44c3980d2307e514a869459f8153cda2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,235 @@
|
||||
using UnityEditor;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
|
||||
namespace XericUIEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// StyleField 的自定义 Inspector 属性绘制器。
|
||||
///
|
||||
/// 默认折叠状态:仅显示样式路径下拉框。
|
||||
/// 展开状态:显示命名空间、路径、资源值类型(有值时)、注解描述(有注解时)。
|
||||
/// 无值/无注解的行不显示。
|
||||
/// </summary>
|
||||
[CustomPropertyDrawer(typeof(StyleField))]
|
||||
public class StyleFieldDrawer : PropertyDrawer
|
||||
{
|
||||
private const float LINE_HEIGHT = 18f;
|
||||
private const float PADDING = 2f;
|
||||
private const float FOLD_BUTTON_WIDTH = 14f;
|
||||
|
||||
/// <summary>折叠状态(按 property path 缓存)</summary>
|
||||
private static readonly System.Collections.Generic.Dictionary<string, bool> s_FoldoutStates
|
||||
= new System.Collections.Generic.Dictionary<string, bool>();
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
|
||||
{
|
||||
string key = GetFoldKey(property);
|
||||
bool expanded = s_FoldoutStates.TryGetValue(key, out bool f) && f;
|
||||
|
||||
if (!expanded)
|
||||
return LINE_HEIGHT + PADDING * 2;
|
||||
|
||||
// 基础:命名空间 + 路径 = 2 行
|
||||
int rowCount = 2;
|
||||
|
||||
// 有值才显示值类型行
|
||||
string ns = GetNamespace(property);
|
||||
string path = property.FindPropertyRelative("StylePath").stringValue;
|
||||
StyleValue val = StyleManager.GetValue(ns, path);
|
||||
if (val != null && val.ValueType != null)
|
||||
rowCount++;
|
||||
|
||||
// 有注解才显示注解行
|
||||
var annotations = StyleManager.GetAnnotations(ns, path);
|
||||
if (annotations != null && annotations.Count > 0)
|
||||
rowCount++;
|
||||
|
||||
return (LINE_HEIGHT + PADDING) * rowCount + PADDING;
|
||||
}
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
|
||||
{
|
||||
string key = GetFoldKey(property);
|
||||
bool expanded = s_FoldoutStates.TryGetValue(key, out bool fold) && fold;
|
||||
|
||||
// 获取子属性
|
||||
SerializedProperty nsProp = property.FindPropertyRelative("Namespace");
|
||||
SerializedProperty pathProp = property.FindPropertyRelative("StylePath");
|
||||
|
||||
// === 标题行:折叠箭头 + 标签 + 路径下拉 ===
|
||||
Rect headerRect = new Rect(position.x, position.y + PADDING, position.width, LINE_HEIGHT);
|
||||
|
||||
// 折叠按钮
|
||||
Rect foldRect = new Rect(headerRect.x, headerRect.y, FOLD_BUTTON_WIDTH, LINE_HEIGHT);
|
||||
bool newFold = EditorGUI.Foldout(foldRect, expanded, GUIContent.none, true);
|
||||
if (newFold != expanded)
|
||||
{
|
||||
s_FoldoutStates[key] = newFold;
|
||||
expanded = newFold;
|
||||
}
|
||||
|
||||
// 标签
|
||||
Rect labelRect = new Rect(headerRect.x + FOLD_BUTTON_WIDTH, headerRect.y,
|
||||
EditorGUIUtility.labelWidth - FOLD_BUTTON_WIDTH, LINE_HEIGHT);
|
||||
EditorGUI.LabelField(labelRect, label);
|
||||
|
||||
// 路径下拉框(始终可见)
|
||||
Rect pathRect = new Rect(headerRect.x + EditorGUIUtility.labelWidth + 4f, headerRect.y,
|
||||
headerRect.width - EditorGUIUtility.labelWidth - 8f, LINE_HEIGHT);
|
||||
|
||||
string currentNs = GetNamespace(property);
|
||||
|
||||
DrawPathDropdown(pathRect, pathProp, currentNs);
|
||||
|
||||
if (!expanded)
|
||||
return;
|
||||
|
||||
// === 展开区域 ===
|
||||
float y = headerRect.y + LINE_HEIGHT + PADDING;
|
||||
float indent = 16f;
|
||||
float fieldWidth = position.width - indent;
|
||||
|
||||
// --- 命名空间 ---
|
||||
Rect nsLabelRect = new Rect(position.x + indent, y, 60f, LINE_HEIGHT);
|
||||
Rect nsFieldRect = new Rect(position.x + indent + 64f, y, fieldWidth - 64f, LINE_HEIGHT);
|
||||
EditorGUI.LabelField(nsLabelRect, "命名空间");
|
||||
DrawNamespaceDropdown(nsFieldRect, nsProp);
|
||||
y += LINE_HEIGHT + PADDING;
|
||||
|
||||
// --- 路径 ---
|
||||
Rect pathLabelRect = new Rect(position.x + indent, y, 60f, LINE_HEIGHT);
|
||||
Rect pathFieldRect = new Rect(position.x + indent + 64f, y, fieldWidth - 64f, LINE_HEIGHT);
|
||||
EditorGUI.LabelField(pathLabelRect, "路径");
|
||||
DrawPathDropdown(pathFieldRect, pathProp, currentNs);
|
||||
y += LINE_HEIGHT + PADDING;
|
||||
|
||||
// --- 资源值类型(仅在有值时显示) ---
|
||||
StyleValue currentValue = StyleManager.GetValue(currentNs, pathProp.stringValue);
|
||||
if (currentValue != null && currentValue.ValueType != null)
|
||||
{
|
||||
Rect valLabelRect = new Rect(position.x + indent, y, 80f, LINE_HEIGHT);
|
||||
Rect valFieldRect = new Rect(position.x + indent + 84f, y, fieldWidth - 84f, LINE_HEIGHT);
|
||||
|
||||
EditorGUI.LabelField(valLabelRect, "值类型");
|
||||
|
||||
string typeDisplay = GetShortTypeName(currentValue.ValueType);
|
||||
string valuePreview = currentValue.RawValue != null
|
||||
? currentValue.RawValue.ToString()
|
||||
: "";
|
||||
|
||||
EditorGUI.LabelField(valFieldRect, $"{typeDisplay} = {valuePreview}");
|
||||
y += LINE_HEIGHT + PADDING;
|
||||
}
|
||||
|
||||
// --- 注解描述(仅在有注解时显示) ---
|
||||
var annotations = StyleManager.GetAnnotations(currentNs, pathProp.stringValue);
|
||||
if (annotations != null && annotations.Count > 0)
|
||||
{
|
||||
Rect annoLabelRect = new Rect(position.x + indent, y, 80f, LINE_HEIGHT);
|
||||
Rect annoFieldRect = new Rect(position.x + indent + 84f, y, fieldWidth - 84f, LINE_HEIGHT);
|
||||
|
||||
EditorGUI.LabelField(annoLabelRect, "注解");
|
||||
|
||||
// 优先显示 @Desc
|
||||
annotations.TryGetValue("Desc", out string desc);
|
||||
if (!string.IsNullOrEmpty(desc))
|
||||
EditorGUI.LabelField(annoFieldRect, desc);
|
||||
else
|
||||
{
|
||||
foreach (var a in annotations)
|
||||
{
|
||||
EditorGUI.LabelField(annoFieldRect, $"@{a.Key}: {a.Value}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 变更检测
|
||||
if (GUI.changed)
|
||||
{
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
|
||||
#region 下拉框绘制
|
||||
|
||||
private void DrawNamespaceDropdown(Rect rect, SerializedProperty nsProp)
|
||||
{
|
||||
var namespaces = StyleManager.GetAvailableNamespaces();
|
||||
if (namespaces.Count == 0)
|
||||
namespaces.Add("default");
|
||||
|
||||
int currentIndex = namespaces.IndexOf(nsProp.stringValue);
|
||||
if (currentIndex < 0) currentIndex = 0;
|
||||
|
||||
int newIndex = EditorGUI.Popup(rect, currentIndex, namespaces.ToArray());
|
||||
if (newIndex >= 0 && newIndex < namespaces.Count)
|
||||
{
|
||||
nsProp.stringValue = namespaces[newIndex];
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPathDropdown(Rect rect, SerializedProperty pathProp, string namespace_)
|
||||
{
|
||||
var paths = StyleManager.GetPathsForNamespace(namespace_);
|
||||
|
||||
var displayOptions = new System.Collections.Generic.List<string>();
|
||||
if (paths.Count == 0)
|
||||
{
|
||||
displayOptions.Add("(无可用路径)");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var p in paths)
|
||||
{
|
||||
StyleValue val = StyleManager.GetValue(namespace_, p);
|
||||
string typeStr = val?.ValueType != null ? GetShortTypeName(val.ValueType) : "?";
|
||||
string valStr = val?.RawValue != null ? val.RawValue.ToString() : "";
|
||||
if (valStr.Length > 20) valStr = valStr.Substring(0, 17) + "...";
|
||||
displayOptions.Add($"{p} [{typeStr}] {valStr}");
|
||||
}
|
||||
}
|
||||
|
||||
int currentIndex = System.Math.Max(0, paths.IndexOf(pathProp.stringValue));
|
||||
|
||||
int newIndex = EditorGUI.Popup(rect, currentIndex, displayOptions.ToArray());
|
||||
if (newIndex >= 0 && newIndex < paths.Count)
|
||||
{
|
||||
pathProp.stringValue = paths[newIndex];
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 工具方法
|
||||
|
||||
private static string GetFoldKey(SerializedProperty property)
|
||||
{
|
||||
return $"{property.serializedObject.targetObject.GetInstanceID()}_{property.propertyPath}";
|
||||
}
|
||||
|
||||
private static string GetNamespace(SerializedProperty property)
|
||||
{
|
||||
string ns = property.FindPropertyRelative("Namespace").stringValue;
|
||||
return string.IsNullOrEmpty(ns) ? "default" : ns;
|
||||
}
|
||||
|
||||
private static string GetShortTypeName(System.Type type)
|
||||
{
|
||||
if (type == null) return "?";
|
||||
switch (type.Name)
|
||||
{
|
||||
case "Single": return "float";
|
||||
case "Int32": return "int";
|
||||
case "String": return "string";
|
||||
case "Boolean": return "bool";
|
||||
default: return type.Name;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ee1f2c601403504faea388e1f91f07b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4110db94896e7434a8eb8182809da2ad
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,172 @@
|
||||
#if UNITY_EDITOR
|
||||
|
||||
using UnityEditor;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using XericUI.XTable.Rendering.Component;
|
||||
|
||||
namespace XericUIEditor.XTable
|
||||
{
|
||||
/// <summary>
|
||||
/// 场景视图表格编辑覆盖工具——选中表格组件后,在世界编辑器视口中显示网格线和编辑辅助 UI。
|
||||
/// 包含合并工具、拖拽引用工具、单元格坐标提示等。
|
||||
/// </summary>
|
||||
public static class XTableEditorOverlay
|
||||
{
|
||||
/// <summary>是否启用覆盖层(可通过菜单开关)</summary>
|
||||
private static bool s_Enabled = true;
|
||||
|
||||
private const string MENU_PATH = "Xeric UI/表格编辑器覆盖层";
|
||||
|
||||
[MenuItem(MENU_PATH, false, 200)]
|
||||
private static void ToggleOverlay()
|
||||
{
|
||||
s_Enabled = !s_Enabled;
|
||||
Menu.SetChecked(MENU_PATH, s_Enabled);
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
|
||||
[MenuItem(MENU_PATH, true)]
|
||||
private static bool ToggleOverlayValidate()
|
||||
{
|
||||
Menu.SetChecked(MENU_PATH, s_Enabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 Scene View 中绘制表格编辑辅助线。
|
||||
/// 调用时机:挂到 SceneView.duringSceneGui 事件上。
|
||||
/// 示例:SceneView.duringSceneGui += OnSceneGUI;
|
||||
/// </summary>
|
||||
public static void OnSceneGUI(SceneView sceneView)
|
||||
{
|
||||
if (!s_Enabled) return;
|
||||
|
||||
// 获取当前选中的表格组件
|
||||
GameObject selected = Selection.activeGameObject;
|
||||
if (selected == null) return;
|
||||
|
||||
XericUIActionTable table = selected.GetComponent<XericUIActionTable>();
|
||||
if (table == null) return;
|
||||
|
||||
RectTransform rect = table.GetComponent<RectTransform>();
|
||||
if (rect == null) return;
|
||||
|
||||
DrawGridOverlay(table, rect);
|
||||
DrawCellInfo(table, rect, sceneView);
|
||||
}
|
||||
|
||||
/// <summary>绘制网格线</summary>
|
||||
private static void DrawGridOverlay(XericUIActionTable table, RectTransform rect)
|
||||
{
|
||||
float[] rowHeights = table.RowHeights;
|
||||
float[] colWidths = table.ColWidths;
|
||||
if (rowHeights == null || colWidths == null) return;
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
rect.GetWorldCorners(corners);
|
||||
|
||||
Vector3 topLeft = corners[1];
|
||||
float totalWidth = rect.rect.width;
|
||||
float totalHeight = rect.rect.height;
|
||||
|
||||
Handles.color = new Color(0.5f, 0.5f, 0.5f, 0.3f);
|
||||
|
||||
// 绘制行分割线
|
||||
float accumulatedY = 0f;
|
||||
for (int r = 0; r < rowHeights.Length && accumulatedY < totalHeight; r++)
|
||||
{
|
||||
float y = topLeft.y - accumulatedY - rowHeights[r];
|
||||
Vector3 start = new Vector3(topLeft.x, y, topLeft.z);
|
||||
Vector3 end = new Vector3(topLeft.x + Mathf.Min(totalWidth, 500f), y, topLeft.z);
|
||||
Handles.DrawLine(start, end);
|
||||
accumulatedY += rowHeights[r];
|
||||
}
|
||||
|
||||
// 绘制列分割线
|
||||
float accumulatedX = 0f;
|
||||
for (int c = 0; c < colWidths.Length && accumulatedX < totalWidth; c++)
|
||||
{
|
||||
float x = topLeft.x + accumulatedX + colWidths[c];
|
||||
Vector3 start = new Vector3(x, topLeft.y, topLeft.z);
|
||||
Vector3 end = new Vector3(x, topLeft.y - Mathf.Min(totalHeight, 500f), topLeft.z);
|
||||
Handles.DrawLine(start, end);
|
||||
accumulatedX += colWidths[c];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>绘制单元格坐标提示</summary>
|
||||
private static void DrawCellInfo(XericUIActionTable table, RectTransform rect, SceneView sceneView)
|
||||
{
|
||||
int selRow = table.SelectedRow;
|
||||
int selCol = table.SelectedCol;
|
||||
if (selRow < 0 || selCol < 0) return;
|
||||
|
||||
float[] rowHeights = table.RowHeights;
|
||||
float[] colWidths = table.ColWidths;
|
||||
if (rowHeights == null || colWidths == null) return;
|
||||
|
||||
// 计算选中单元格的世界位置
|
||||
float cellX = 0f;
|
||||
for (int c = 0; c < selCol && c < colWidths.Length; c++)
|
||||
cellX += colWidths[c];
|
||||
|
||||
float cellY = 0f;
|
||||
for (int r = 0; r < selRow && r < rowHeights.Length; r++)
|
||||
cellY += rowHeights[r];
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
rect.GetWorldCorners(corners);
|
||||
Vector3 topLeft = corners[1];
|
||||
|
||||
Vector3 cellCenter = new Vector3(
|
||||
topLeft.x + cellX + (selCol < colWidths.Length ? colWidths[selCol] * 0.5f : 0),
|
||||
topLeft.y - cellY - (selRow < rowHeights.Length ? rowHeights[selRow] * 0.5f : 0),
|
||||
topLeft.z);
|
||||
|
||||
// 绘制选中高亮框
|
||||
Handles.color = new Color(0.2f, 0.6f, 1f, 0.5f);
|
||||
float cellW = selCol < colWidths.Length ? colWidths[selCol] : 50f;
|
||||
float cellH = selRow < rowHeights.Length ? rowHeights[selRow] : 30f;
|
||||
Vector3 cellTL = new Vector3(cellCenter.x - cellW * 0.5f, cellCenter.y + cellH * 0.5f, cellCenter.z);
|
||||
Vector3 cellTR = new Vector3(cellCenter.x + cellW * 0.5f, cellCenter.y + cellH * 0.5f, cellCenter.z);
|
||||
Vector3 cellBR = new Vector3(cellCenter.x + cellW * 0.5f, cellCenter.y - cellH * 0.5f, cellCenter.z);
|
||||
Vector3 cellBL = new Vector3(cellCenter.x - cellW * 0.5f, cellCenter.y - cellH * 0.5f, cellCenter.z);
|
||||
Handles.DrawSolidRectangleWithOutline(
|
||||
new Vector3[] { cellTL, cellTR, cellBR, cellBL },
|
||||
new Color(0.2f, 0.6f, 1f, 0.2f),
|
||||
new Color(0.2f, 0.6f, 1f, 0.8f));
|
||||
|
||||
// 显示单元格坐标标签
|
||||
Handles.Label(cellCenter + Vector3.up * 10f,
|
||||
$"({selRow}, {selCol})",
|
||||
new GUIStyle(EditorStyles.label)
|
||||
{
|
||||
normal = { textColor = Color.white },
|
||||
fontStyle = FontStyle.Bold
|
||||
});
|
||||
|
||||
// 工具栏按钮(在 Scene View 顶部绘制)
|
||||
Handles.BeginGUI();
|
||||
GUILayout.BeginArea(new Rect(10, 10, 200, 80));
|
||||
GUILayout.BeginVertical("表格编辑工具", GUI.skin.window);
|
||||
|
||||
if (GUILayout.Button("合并选区"))
|
||||
{
|
||||
Debug.Log($"[XTable] 合并起始于 ({selRow}, {selCol})");
|
||||
}
|
||||
|
||||
if (GUILayout.Button("取消合并"))
|
||||
{
|
||||
Debug.Log($"[XTable] 取消合并 ({selRow}, {selCol})");
|
||||
}
|
||||
|
||||
GUILayout.EndVertical();
|
||||
GUILayout.EndArea();
|
||||
Handles.EndGUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 220060be4d5df6a40afa23e4a7af1d64
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,334 @@
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericUI.XTable.Rendering.Component;
|
||||
|
||||
namespace XericUIEditor.XTable
|
||||
{
|
||||
/// <summary>
|
||||
/// XericUIActionTable 的 Inspector 编辑器——
|
||||
/// 折叠分组:[样式设定] [尺寸设定] [编辑测试] [数据浏览]。
|
||||
/// 使用 EditorGUILayout.Foldout 避免与数组 PropertyField 内部的折叠冲突。
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(XericUIActionTable))]
|
||||
public class XericUIActionTableEditor : Editor
|
||||
{
|
||||
private XericUIActionTable m_Table;
|
||||
|
||||
private SerializedProperty m_RowHeightsProp;
|
||||
private SerializedProperty m_ColWidthsProp;
|
||||
private SerializedProperty m_DefaultStyleNsProp;
|
||||
private SerializedProperty m_ScrollRectProp;
|
||||
|
||||
// 折叠状态
|
||||
private static bool s_StyleFoldout = true;
|
||||
private static bool s_SizeFoldout = true;
|
||||
private static bool s_EditTestFoldout;
|
||||
private static bool s_DataBrowseFoldout;
|
||||
|
||||
// 编辑测试参数
|
||||
private int m_BatchRowCount = 10;
|
||||
private int m_BatchColCount = 10;
|
||||
private float m_BatchCellHeight = 40f;
|
||||
private float m_BatchCellWidth = 100f;
|
||||
|
||||
private int m_MergeStartRow;
|
||||
private int m_MergeStartCol;
|
||||
private int m_MergeRowSpan = 2;
|
||||
private int m_MergeColSpan = 2;
|
||||
|
||||
private int m_EditRow;
|
||||
private int m_EditCol;
|
||||
private string m_EditText = "";
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
m_Table = (XericUIActionTable)target;
|
||||
|
||||
m_RowHeightsProp = serializedObject.FindProperty("m_RowHeights");
|
||||
m_ColWidthsProp = serializedObject.FindProperty("m_ColWidths");
|
||||
m_DefaultStyleNsProp = serializedObject.FindProperty("m_DefaultStyleNamespace");
|
||||
m_ScrollRectProp = serializedObject.FindProperty("m_ScrollRect");
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
// ===== 样式设定 =====
|
||||
s_StyleFoldout = EditorGUILayout.Foldout(s_StyleFoldout, "样式设定", true);
|
||||
if (s_StyleFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
if (m_DefaultStyleNsProp != null)
|
||||
EditorGUILayout.PropertyField(m_DefaultStyleNsProp, new GUIContent("默认命名空间"));
|
||||
if (m_ScrollRectProp != null)
|
||||
EditorGUILayout.PropertyField(m_ScrollRectProp, new GUIContent("ScrollRect"));
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
// ===== 尺寸设定 =====
|
||||
EditorGUILayout.Space(4);
|
||||
s_SizeFoldout = EditorGUILayout.Foldout(s_SizeFoldout, "尺寸设定", true);
|
||||
if (s_SizeFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
if (m_RowHeightsProp != null)
|
||||
EditorGUILayout.PropertyField(m_RowHeightsProp, new GUIContent("行高"), true);
|
||||
if (m_ColWidthsProp != null)
|
||||
EditorGUILayout.PropertyField(m_ColWidthsProp, new GUIContent("列宽"), true);
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
// ===== 编辑测试 =====
|
||||
EditorGUILayout.Space(4);
|
||||
s_EditTestFoldout = EditorGUILayout.Foldout(s_EditTestFoldout, "编辑测试", true);
|
||||
if (s_EditTestFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.LabelField("批量生成尺寸", EditorStyles.miniBoldLabel);
|
||||
m_BatchRowCount = EditorGUILayout.IntField("行数", m_BatchRowCount);
|
||||
m_BatchColCount = EditorGUILayout.IntField("列数", m_BatchColCount);
|
||||
m_BatchCellHeight = EditorGUILayout.FloatField("单元格高度", m_BatchCellHeight);
|
||||
m_BatchCellWidth = EditorGUILayout.FloatField("单元格宽度", m_BatchCellWidth);
|
||||
if (GUILayout.Button("应用批量尺寸", GUILayout.Height(22)))
|
||||
ApplyBatchRowColSizes();
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
EditorGUILayout.LabelField("合并单元格", EditorStyles.miniBoldLabel);
|
||||
m_MergeStartRow = EditorGUILayout.IntField("起始行", m_MergeStartRow);
|
||||
m_MergeStartCol = EditorGUILayout.IntField("起始列", m_MergeStartCol);
|
||||
m_MergeRowSpan = EditorGUILayout.IntField("行跨度", m_MergeRowSpan);
|
||||
m_MergeColSpan = EditorGUILayout.IntField("列跨度", m_MergeColSpan);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("合并", GUILayout.Height(22)))
|
||||
EditorApplication.delayCall += () => m_Table.MergeCells(m_MergeStartRow, m_MergeStartCol, m_MergeRowSpan, m_MergeColSpan);
|
||||
if (GUILayout.Button("取消合并", GUILayout.Height(22)))
|
||||
EditorApplication.delayCall += () => m_Table.UnmergeCells(m_MergeStartRow, m_MergeStartCol);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.Space(6);
|
||||
|
||||
EditorGUILayout.LabelField("单元格编辑", EditorStyles.miniBoldLabel);
|
||||
m_EditRow = EditorGUILayout.IntField("行", m_EditRow);
|
||||
m_EditCol = EditorGUILayout.IntField("列", m_EditCol);
|
||||
|
||||
var existing = m_Table.GetCellData(m_EditRow, m_EditCol);
|
||||
if (existing != null && !string.IsNullOrEmpty(existing.Text) && string.IsNullOrEmpty(m_EditText))
|
||||
m_EditText = existing.Text;
|
||||
|
||||
m_EditText = EditorGUILayout.TextField("文本内容", m_EditText);
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("应用文本", GUILayout.Height(22)))
|
||||
EditorApplication.delayCall += () => m_Table.SetCellText(m_EditRow, m_EditCol, m_EditText);
|
||||
if (GUILayout.Button("选中此格", GUILayout.Height(22)))
|
||||
EditorApplication.delayCall += () => m_Table.SelectCell(m_EditRow, m_EditCol);
|
||||
if (GUILayout.Button("清除选中", GUILayout.Height(22)))
|
||||
EditorApplication.delayCall += () => m_Table.ClearSelection();
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
// ===== 数据浏览 =====
|
||||
EditorGUILayout.Space(4);
|
||||
s_DataBrowseFoldout = EditorGUILayout.Foldout(s_DataBrowseFoldout, "数据浏览", true);
|
||||
if (s_DataBrowseFoldout)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
if (m_Table.TableData != null)
|
||||
{
|
||||
EditorGUI.BeginDisabledGroup(true);
|
||||
EditorGUILayout.IntField("块数量", m_Table.TableData.BlockCount);
|
||||
EditorGUILayout.TextField("块尺寸",
|
||||
$"{m_Table.TableData.BlockSizeX} × {m_Table.TableData.BlockSizeY}");
|
||||
EditorGUILayout.TextField("选中单元格",
|
||||
m_Table.SelectedRow >= 0 ? $"({m_Table.SelectedRow}, {m_Table.SelectedCol})" : "(未选中)");
|
||||
EditorGUI.EndDisabledGroup();
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox("TableData 为空——Awake() 执行后会自动创建默认实例。", MessageType.Info);
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(4);
|
||||
if (GUILayout.Button("手动刷新视图", GUILayout.Height(22)))
|
||||
EditorApplication.delayCall += () => m_Table.RefreshView();
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
private void ApplyBatchRowColSizes()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
if (m_RowHeightsProp != null)
|
||||
{
|
||||
m_RowHeightsProp.arraySize = m_BatchRowCount;
|
||||
for (int i = 0; i < m_BatchRowCount; i++)
|
||||
m_RowHeightsProp.GetArrayElementAtIndex(i).floatValue = m_BatchCellHeight;
|
||||
}
|
||||
|
||||
if (m_ColWidthsProp != null)
|
||||
{
|
||||
m_ColWidthsProp.arraySize = m_BatchColCount;
|
||||
for (int i = 0; i < m_BatchColCount; i++)
|
||||
m_ColWidthsProp.GetArrayElementAtIndex(i).floatValue = m_BatchCellWidth;
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
#region Hierarchy 创建菜单
|
||||
|
||||
[MenuItem("GameObject/Xeric UI/Table/Xeric UI Action Table", false, 60)]
|
||||
private static void CreateTable(MenuCommand menuCommand)
|
||||
{
|
||||
// 1. 创建表格内容节点
|
||||
GameObject contentGo = new GameObject("Content",
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(XericUIActionTable));
|
||||
RectTransform contentRt = contentGo.GetComponent<RectTransform>();
|
||||
contentRt.anchorMin = new Vector2(0, 1);
|
||||
contentRt.anchorMax = new Vector2(0, 1);
|
||||
contentRt.pivot = new Vector2(0, 1);
|
||||
contentRt.anchoredPosition = Vector2.zero;
|
||||
contentRt.sizeDelta = new Vector2(800, 600);
|
||||
XericUIActionTable table = contentGo.GetComponent<XericUIActionTable>();
|
||||
|
||||
// 2. 创建 Viewport(Mask 裁剪区域)
|
||||
GameObject viewportGo = new GameObject("Viewport",
|
||||
typeof(RectTransform), typeof(Image), typeof(Mask));
|
||||
viewportGo.GetComponent<Image>().color = new Color(1, 1, 1, 0.01f);
|
||||
RectTransform vpRt = viewportGo.GetComponent<RectTransform>();
|
||||
vpRt.anchorMin = Vector2.zero;
|
||||
vpRt.anchorMax = Vector2.one;
|
||||
vpRt.sizeDelta = Vector2.zero;
|
||||
contentRt.SetParent(vpRt, false);
|
||||
|
||||
// 3. 创建水平滚动条
|
||||
GameObject hScrollbarGo = new GameObject("Scrollbar Horizontal",
|
||||
typeof(RectTransform), typeof(Scrollbar), typeof(Image));
|
||||
RectTransform hBarRt = hScrollbarGo.GetComponent<RectTransform>();
|
||||
hBarRt.anchorMin = new Vector2(0, 0);
|
||||
hBarRt.anchorMax = new Vector2(1, 0);
|
||||
hBarRt.pivot = new Vector2(0.5f, 0);
|
||||
hBarRt.anchoredPosition = new Vector2(0, 4);
|
||||
hBarRt.sizeDelta = new Vector2(-16, 20);
|
||||
Image hBarImg = hScrollbarGo.GetComponent<Image>();
|
||||
hBarImg.color = new Color(0.15f, 0.15f, 0.15f, 0.5f);
|
||||
Scrollbar hBar = hScrollbarGo.GetComponent<Scrollbar>();
|
||||
hBar.direction = Scrollbar.Direction.LeftToRight;
|
||||
|
||||
GameObject hSlidingGo = new GameObject("Sliding Area",
|
||||
typeof(RectTransform));
|
||||
hSlidingGo.transform.SetParent(hScrollbarGo.transform, false);
|
||||
RectTransform hSlidingRt = hSlidingGo.GetComponent<RectTransform>();
|
||||
hSlidingRt.anchorMin = Vector2.zero;
|
||||
hSlidingRt.anchorMax = Vector2.one;
|
||||
hSlidingRt.sizeDelta = new Vector2(-20, -20);
|
||||
|
||||
GameObject hHandleGo = new GameObject("Handle",
|
||||
typeof(RectTransform), typeof(Image));
|
||||
hHandleGo.transform.SetParent(hSlidingGo.transform, false);
|
||||
RectTransform hHandleRt = hHandleGo.GetComponent<RectTransform>();
|
||||
hHandleRt.anchorMin = Vector2.zero;
|
||||
hHandleRt.anchorMax = Vector2.one;
|
||||
hHandleRt.sizeDelta = new Vector2(-20, -20);
|
||||
hHandleGo.GetComponent<Image>().color = new Color(0.4f, 0.4f, 0.4f, 0.8f);
|
||||
hBar.handleRect = hHandleRt;
|
||||
|
||||
// 4. 创建垂直滚动条
|
||||
GameObject vScrollbarGo = new GameObject("Scrollbar Vertical",
|
||||
typeof(RectTransform), typeof(Scrollbar), typeof(Image));
|
||||
RectTransform vBarRt = vScrollbarGo.GetComponent<RectTransform>();
|
||||
vBarRt.anchorMin = new Vector2(1, 0);
|
||||
vBarRt.anchorMax = new Vector2(1, 1);
|
||||
vBarRt.pivot = new Vector2(1, 0.5f);
|
||||
vBarRt.anchoredPosition = new Vector2(-4, 0);
|
||||
vBarRt.sizeDelta = new Vector2(20, -16);
|
||||
Image vBarImg = vScrollbarGo.GetComponent<Image>();
|
||||
vBarImg.color = new Color(0.15f, 0.15f, 0.15f, 0.5f);
|
||||
Scrollbar vBar = vScrollbarGo.GetComponent<Scrollbar>();
|
||||
vBar.direction = Scrollbar.Direction.BottomToTop;
|
||||
|
||||
GameObject vSlidingGo = new GameObject("Sliding Area",
|
||||
typeof(RectTransform));
|
||||
vSlidingGo.transform.SetParent(vScrollbarGo.transform, false);
|
||||
RectTransform vSlidingRt = vSlidingGo.GetComponent<RectTransform>();
|
||||
vSlidingRt.anchorMin = Vector2.zero;
|
||||
vSlidingRt.anchorMax = Vector2.one;
|
||||
vSlidingRt.sizeDelta = new Vector2(-20, -20);
|
||||
|
||||
GameObject vHandleGo = new GameObject("Handle",
|
||||
typeof(RectTransform), typeof(Image));
|
||||
vHandleGo.transform.SetParent(vSlidingGo.transform, false);
|
||||
RectTransform vHandleRt = vHandleGo.GetComponent<RectTransform>();
|
||||
vHandleRt.anchorMin = Vector2.zero;
|
||||
vHandleRt.anchorMax = Vector2.one;
|
||||
vHandleRt.sizeDelta = new Vector2(-20, -20);
|
||||
vHandleGo.GetComponent<Image>().color = new Color(0.4f, 0.4f, 0.4f, 0.8f);
|
||||
vBar.handleRect = vHandleRt;
|
||||
|
||||
// 5. 创建 ScrollView 根节点
|
||||
GameObject scrollGo = new GameObject("Xeric UI Action Table",
|
||||
typeof(RectTransform), typeof(ScrollRect), typeof(Image));
|
||||
RectTransform scrollRt = scrollGo.GetComponent<RectTransform>();
|
||||
scrollRt.sizeDelta = new Vector2(800, 600);
|
||||
scrollGo.GetComponent<Image>().color = new Color(0.1f, 0.1f, 0.1f, 0.3f);
|
||||
|
||||
ScrollRect scrollRect = scrollGo.GetComponent<ScrollRect>();
|
||||
scrollRect.content = contentRt;
|
||||
scrollRect.viewport = vpRt;
|
||||
scrollRect.horizontalScrollbar = hBar;
|
||||
scrollRect.verticalScrollbar = vBar;
|
||||
scrollRect.horizontalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide;
|
||||
scrollRect.verticalScrollbarVisibility = ScrollRect.ScrollbarVisibility.AutoHide;
|
||||
scrollRect.movementType = ScrollRect.MovementType.Clamped;
|
||||
|
||||
// 组装层级
|
||||
vpRt.SetParent(scrollRt, false);
|
||||
hScrollbarGo.transform.SetParent(scrollRt, false);
|
||||
vScrollbarGo.transform.SetParent(scrollRt, false);
|
||||
|
||||
// 绑定 ScrollRect 到表格(通过 SerializedObject 正确回写)
|
||||
using (SerializedObject so = new SerializedObject(table))
|
||||
{
|
||||
SerializedProperty prop = so.FindProperty("m_ScrollRect");
|
||||
if (prop != null)
|
||||
{
|
||||
prop.objectReferenceValue = scrollRect;
|
||||
so.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
|
||||
// 挂载到 Canvas
|
||||
GameObject parent = GetOrCreateCanvas();
|
||||
scrollRt.SetParent(parent.transform, false);
|
||||
|
||||
Undo.RegisterCreatedObjectUndo(scrollGo, "Create Xeric UI Action Table");
|
||||
Selection.activeGameObject = scrollGo;
|
||||
}
|
||||
|
||||
private static GameObject GetOrCreateCanvas()
|
||||
{
|
||||
Canvas canvas = Object.FindObjectOfType<Canvas>();
|
||||
if (canvas != null && canvas.isRootCanvas)
|
||||
return canvas.gameObject;
|
||||
|
||||
GameObject canvasGo = new GameObject("Canvas",
|
||||
typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
|
||||
canvasGo.GetComponent<Canvas>().renderMode = RenderMode.ScreenSpaceOverlay;
|
||||
return canvasGo;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a9a0aa38413f5264a97d77cb6bdfde8d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,371 @@
|
||||
using UnityEditor;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using XericUI.XTable.Rendering.Component;
|
||||
|
||||
namespace XericUIEditor.XTable
|
||||
{
|
||||
/// <summary>
|
||||
/// Scene View 表格编辑覆盖层——
|
||||
/// 左侧行索引、顶部列索引、底部输入框。
|
||||
/// 点击行列交叉区域选中单元格,底部输入框编辑文本。
|
||||
/// 不创建任何场景对象,通过 Handles.BeginGUI/EndGUI 绘制。
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
public static class XericUIActionTableSceneEditor
|
||||
{
|
||||
private static bool s_Enabled;
|
||||
private const string ENABLED_KEY = "XericUI_TableSceneEditor_Enabled";
|
||||
|
||||
private static string s_EditText = "";
|
||||
|
||||
private static readonly Color s_LabelBg = new Color(0.12f, 0.12f, 0.18f, 0.9f);
|
||||
private static readonly Color s_SelectedLabel = new Color(0.2f, 0.5f, 0.9f, 0.6f);
|
||||
private static readonly Color s_LabelText = new Color(0.75f, 0.75f, 0.8f);
|
||||
private static readonly Color s_InputBg = new Color(0.08f, 0.08f, 0.14f, 0.92f);
|
||||
|
||||
private const float ROW_H = 22f;
|
||||
private const float COL_W = 36f;
|
||||
private const float MARGIN = 3f;
|
||||
|
||||
static XericUIActionTableSceneEditor()
|
||||
{
|
||||
s_Enabled = EditorPrefs.GetBool(ENABLED_KEY, true);
|
||||
SceneView.duringSceneGui += OnSceneGUI;
|
||||
}
|
||||
|
||||
[MenuItem("Xeric UI/表格编辑器覆盖层", false, 200)]
|
||||
private static void ToggleEnabled()
|
||||
{
|
||||
s_Enabled = !s_Enabled;
|
||||
EditorPrefs.SetBool(ENABLED_KEY, s_Enabled);
|
||||
Menu.SetChecked("Xeric UI/表格编辑器覆盖层", s_Enabled);
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
|
||||
[MenuItem("Xeric UI/表格编辑器覆盖层", true)]
|
||||
private static bool ToggleEnabledValidate()
|
||||
{
|
||||
Menu.SetChecked("Xeric UI/表格编辑器覆盖层", s_Enabled);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void OnSceneGUI(SceneView sceneView)
|
||||
{
|
||||
if (!s_Enabled) return;
|
||||
|
||||
GameObject sel = Selection.activeGameObject;
|
||||
if (sel == null) return;
|
||||
|
||||
// 不管选了表格祖上还是子孙,都能找到组件
|
||||
XericUIActionTable table = sel.GetComponentInChildren<XericUIActionTable>();
|
||||
if (table == null)
|
||||
table = sel.GetComponentInParent<XericUIActionTable>();
|
||||
if (table == null || !table.isActiveAndEnabled) return;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
Canvas.ForceUpdateCanvases();
|
||||
#endif
|
||||
|
||||
RectTransform tableRt = table.GetComponent<RectTransform>();
|
||||
float[] rh = table.RowHeights;
|
||||
float[] cw = table.ColWidths;
|
||||
if (rh == null || cw == null || rh.Length == 0 || cw.Length == 0) return;
|
||||
|
||||
// GetWorldCorners 返回世界坐标,topleft = corners[1]
|
||||
Vector3[] corners = new Vector3[4];
|
||||
tableRt.GetWorldCorners(corners);
|
||||
|
||||
Camera svCam = sceneView.camera;
|
||||
Rect camRect = svCam.pixelRect;
|
||||
|
||||
// 对 ScreenSpaceOverlay Canvas,世界坐标即屏幕像素坐标
|
||||
// 需要映射到 SceneView 本地 GUI 坐标
|
||||
Vector2 guiTL = ScreenToSceneViewGUI(corners[1], camRect);
|
||||
Vector2 guiBR = ScreenToSceneViewGUI(corners[3], camRect);
|
||||
|
||||
float tableW = guiBR.x - guiTL.x;
|
||||
float tableH = guiBR.y - guiTL.y;
|
||||
if (tableW <= 0 || tableH <= 0) return;
|
||||
|
||||
float totalW = 0, totalH = 0;
|
||||
foreach (float w in cw) totalW += w;
|
||||
foreach (float h in rh) totalH += h;
|
||||
if (totalW <= 0 || totalH <= 0) return;
|
||||
|
||||
float xScale = tableW / totalW;
|
||||
float yScale = tableH / totalH;
|
||||
|
||||
// 表格区域(GUI 坐标,Y 向下)
|
||||
Rect tableRect = new Rect(guiTL.x, guiTL.y, tableW, tableH);
|
||||
|
||||
Handles.BeginGUI();
|
||||
|
||||
DrawRowLabels(rh, tableRect, xScale, yScale, table);
|
||||
DrawColumnLabels(cw, tableRect, xScale, yScale, table);
|
||||
DrawGridLines(rh, cw, tableRect, xScale, yScale);
|
||||
DrawSelectionHighlight(rh, cw, tableRect, xScale, yScale, table);
|
||||
DrawInputBar(table, tableRect.x, tableRect.y + tableRect.height + MARGIN);
|
||||
HandleInput(table, rh, cw, tableRect, xScale, yScale);
|
||||
|
||||
Handles.EndGUI();
|
||||
}
|
||||
|
||||
/// <summary>屏幕坐标转 SceneView 本地 GUI 坐标</summary>
|
||||
private static Vector2 ScreenToSceneViewGUI(Vector2 screenPos, Rect camPixelRect)
|
||||
{
|
||||
return new Vector2(
|
||||
screenPos.x - camPixelRect.x,
|
||||
camPixelRect.yMax - screenPos.y
|
||||
);
|
||||
}
|
||||
|
||||
#region 行标签
|
||||
|
||||
private static void DrawRowLabels(float[] rh, Rect tableRect,
|
||||
float xScale, float yScale, XericUIActionTable table)
|
||||
{
|
||||
var style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = 10,
|
||||
normal = { textColor = s_LabelText },
|
||||
alignment = TextAnchor.MiddleCenter
|
||||
};
|
||||
|
||||
float y = tableRect.y;
|
||||
for (int r = 0; r < rh.Length; r++)
|
||||
{
|
||||
float cellH = rh[r] * yScale;
|
||||
if (cellH < 2) { y += cellH; continue; }
|
||||
|
||||
Rect labelRect = new Rect(
|
||||
tableRect.x - COL_W - MARGIN,
|
||||
y,
|
||||
COL_W,
|
||||
cellH);
|
||||
|
||||
bool isSelected = (table.SelectedRow == r);
|
||||
EditorGUI.DrawRect(labelRect, isSelected ? s_SelectedLabel : s_LabelBg);
|
||||
GUI.Label(labelRect, $"{r}", style);
|
||||
|
||||
if (Event.current.type == EventType.MouseDown &&
|
||||
Event.current.button == 0 &&
|
||||
labelRect.Contains(Event.current.mousePosition))
|
||||
{
|
||||
int col = table.SelectedCol;
|
||||
if (col < 0) col = 0;
|
||||
table.SelectCell(r, col);
|
||||
table.RefreshView();
|
||||
UpdateEditText(table, r, col);
|
||||
Event.current.Use();
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
|
||||
y += cellH;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 列标签
|
||||
|
||||
private static void DrawColumnLabels(float[] cw, Rect tableRect,
|
||||
float xScale, float yScale, XericUIActionTable table)
|
||||
{
|
||||
var style = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = 10,
|
||||
normal = { textColor = s_LabelText },
|
||||
alignment = TextAnchor.MiddleCenter
|
||||
};
|
||||
|
||||
float x = tableRect.x;
|
||||
for (int c = 0; c < cw.Length; c++)
|
||||
{
|
||||
float cellW = cw[c] * xScale;
|
||||
if (cellW < 2) { x += cellW; continue; }
|
||||
|
||||
Rect labelRect = new Rect(
|
||||
x,
|
||||
tableRect.y - ROW_H - MARGIN,
|
||||
cellW,
|
||||
ROW_H);
|
||||
|
||||
bool isSelected = (table.SelectedCol == c);
|
||||
EditorGUI.DrawRect(labelRect, isSelected ? s_SelectedLabel : s_LabelBg);
|
||||
string text = cellW > 40 ? $"列{c}" : (cellW > 18 ? $"{c}" : "");
|
||||
GUI.Label(labelRect, text, style);
|
||||
|
||||
if (Event.current.type == EventType.MouseDown &&
|
||||
Event.current.button == 0 &&
|
||||
labelRect.Contains(Event.current.mousePosition))
|
||||
{
|
||||
int row = table.SelectedRow;
|
||||
if (row < 0) row = 0;
|
||||
table.SelectCell(row, c);
|
||||
table.RefreshView();
|
||||
UpdateEditText(table, row, c);
|
||||
Event.current.Use();
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
|
||||
x += cellW;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 网格线
|
||||
|
||||
private static void DrawGridLines(float[] rh, float[] cw, Rect tr,
|
||||
float xScale, float yScale)
|
||||
{
|
||||
Color lineColor = new Color(0.5f, 0.5f, 0.55f, 0.18f);
|
||||
|
||||
float y = tr.y;
|
||||
for (int r = 0; r < rh.Length; r++)
|
||||
{
|
||||
y += rh[r] * yScale;
|
||||
EditorGUI.DrawRect(new Rect(tr.x, y, tr.width, 1), lineColor);
|
||||
}
|
||||
|
||||
float x = tr.x;
|
||||
for (int c = 0; c < cw.Length; c++)
|
||||
{
|
||||
x += cw[c] * xScale;
|
||||
EditorGUI.DrawRect(new Rect(x, tr.y, 1, tr.height), lineColor);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 选中高亮
|
||||
|
||||
private static void DrawSelectionHighlight(float[] rh, float[] cw, Rect tr,
|
||||
float xScale, float yScale, XericUIActionTable table)
|
||||
{
|
||||
int sr = table.SelectedRow;
|
||||
int sc = table.SelectedCol;
|
||||
if (sr < 0 || sc < 0 || sr >= rh.Length || sc >= cw.Length) return;
|
||||
|
||||
float sx = tr.x;
|
||||
for (int c = 0; c < sc; c++) sx += cw[c] * xScale;
|
||||
|
||||
float sy = tr.y;
|
||||
for (int r = 0; r < sr; r++) sy += rh[r] * yScale;
|
||||
|
||||
float sw = cw[sc] * xScale;
|
||||
float sh = rh[sr] * yScale;
|
||||
|
||||
EditorGUI.DrawRect(new Rect(sx + 1, sy + 1, sw - 2, sh - 2),
|
||||
new Color(0.18f, 0.45f, 0.85f, 0.25f));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 底部输入框
|
||||
|
||||
private static void DrawInputBar(XericUIActionTable table, float x, float y)
|
||||
{
|
||||
Rect barRect = new Rect(x, y, 400, 24);
|
||||
EditorGUI.DrawRect(barRect, s_InputBg);
|
||||
|
||||
if (table.SelectedRow >= 0 && table.SelectedCol >= 0)
|
||||
{
|
||||
var labelStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = 11,
|
||||
normal = { textColor = s_LabelText },
|
||||
alignment = TextAnchor.MiddleCenter
|
||||
};
|
||||
GUI.Label(new Rect(barRect.x + 4, barRect.y, 50, barRect.height),
|
||||
$"({table.SelectedRow},{table.SelectedCol})", labelStyle);
|
||||
|
||||
var inputStyle = new GUIStyle(GUI.skin.textField)
|
||||
{
|
||||
fontSize = 12,
|
||||
margin = new RectOffset(2, 2, 2, 2)
|
||||
};
|
||||
Rect inputRect = new Rect(barRect.x + 56, barRect.y + 2, 180, barRect.height - 4);
|
||||
s_EditText = GUI.TextField(inputRect, s_EditText, inputStyle);
|
||||
|
||||
var btnStyle = new GUIStyle(GUI.skin.button)
|
||||
{
|
||||
fontSize = 11,
|
||||
margin = new RectOffset(2, 2, 2, 2),
|
||||
padding = new RectOffset(4, 4, 2, 2)
|
||||
};
|
||||
if (GUI.Button(new Rect(barRect.x + 242, barRect.y + 2, 50, barRect.height - 4),
|
||||
"应用", btnStyle))
|
||||
{
|
||||
ApplyEdit(table);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var hintStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
fontSize = 10,
|
||||
normal = { textColor = new Color(0.5f, 0.5f, 0.5f) },
|
||||
alignment = TextAnchor.MiddleCenter
|
||||
};
|
||||
GUI.Label(barRect, "点击左侧行号或上方列号选中单元格", hintStyle);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyEdit(XericUIActionTable table)
|
||||
{
|
||||
if (table.SelectedRow < 0 || table.SelectedCol < 0) return;
|
||||
table.SetCellText(table.SelectedRow, table.SelectedCol, s_EditText);
|
||||
table.RefreshView();
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
|
||||
private static void UpdateEditText(XericUIActionTable table, int row, int col)
|
||||
{
|
||||
var data = table.GetCellData(row, col);
|
||||
s_EditText = data != null ? data.Text ?? "" : "";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 表格区域点击
|
||||
|
||||
private static void HandleInput(XericUIActionTable table,
|
||||
float[] rh, float[] cw, Rect tr,
|
||||
float xScale, float yScale)
|
||||
{
|
||||
Event e = Event.current;
|
||||
if (e.type != EventType.MouseDown || e.button != 0) return;
|
||||
if (!tr.Contains(e.mousePosition)) return;
|
||||
|
||||
float rx = (e.mousePosition.x - tr.x) / xScale;
|
||||
float ry = (e.mousePosition.y - tr.y) / yScale;
|
||||
|
||||
int col = IndexFromCoord(rx, cw);
|
||||
int row = IndexFromCoord(ry, rh);
|
||||
if (row < 0 || col < 0) return;
|
||||
|
||||
table.SelectCell(row, col);
|
||||
table.RefreshView();
|
||||
UpdateEditText(table, row, col);
|
||||
|
||||
e.Use();
|
||||
SceneView.RepaintAll();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static int IndexFromCoord(float coord, float[] sizes)
|
||||
{
|
||||
float acc = 0;
|
||||
for (int i = 0; i < sizes.Length; i++)
|
||||
{
|
||||
if (coord >= acc && coord < acc + sizes[i]) return i;
|
||||
acc += sizes[i];
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e04322ddc12583c4187e283c0752bcf6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4969d4a2ba6af0409896898d9854d05
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,12 @@
|
||||
// ==================================================
|
||||
// Xeric Style Sheet (XSSS) - 超级干燥样式表 - 默认样式
|
||||
// ==================================================
|
||||
|
||||
namespace: default
|
||||
|
||||
/button/TargetGraphic/ColorTint:UnityEngine.UI.ColorBlock/ {
|
||||
.NormalColor = (1,1,1,1)
|
||||
.HighlightedColor = (0.96,0.96,0.96,1)
|
||||
.PressedColor = (0.78,0.78,0.78,1)
|
||||
.DisabledColor = (0.78,0.78,0.78,0.5)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5b4cf6e97fa28b429cbb9088690bfc8
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace XericUI.Core.StyleSheetUI
|
||||
{
|
||||
/// <summary>
|
||||
/// UI 交互状态枚举——仿 Selectable.SelectionState(protected 无法外部引用)。
|
||||
/// 使用时通过 (StyleSheetSelectionState)(int)selectableState 转换。
|
||||
/// </summary>
|
||||
public enum StyleSheetSelectionState
|
||||
{
|
||||
/// <summary>正常状态</summary>
|
||||
Normal,
|
||||
|
||||
/// <summary>高亮(鼠标悬停)状态</summary>
|
||||
Highlighted,
|
||||
|
||||
/// <summary>按下状态</summary>
|
||||
Pressed,
|
||||
|
||||
/// <summary>选中状态</summary>
|
||||
Selected,
|
||||
|
||||
/// <summary>禁用状态</summary>
|
||||
Disabled,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0ffbcf7800c40b45b11f9e9fdd88a97
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
|
||||
namespace XericUI.Core.StyleSheetUI
|
||||
{
|
||||
/// <summary>
|
||||
/// 样式表驱动的 UI 交互状态块——仿 ColorBlock 设计。
|
||||
/// 集中管理 Normal/Highlighted/Pressed/Selected/Disabled 五种状态的 StyleField。
|
||||
/// 职责单一:仅保存一组样式属性(如仅颜色、或仅纹理)。
|
||||
/// 在组件上声明多个实例分别实现颜色切换和纹理切换。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StyleSheetState
|
||||
{
|
||||
#region 序列化字段
|
||||
|
||||
[Header("五态样式路径")]
|
||||
[SerializeField]
|
||||
[Tooltip("正常状态")]
|
||||
private StyleField m_Normal = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("高亮状态")]
|
||||
private StyleField m_Highlighted = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("按下状态")]
|
||||
private StyleField m_Pressed = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("选中状态")]
|
||||
private StyleField m_Selected = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("禁用状态")]
|
||||
private StyleField m_Disabled = new StyleField();
|
||||
|
||||
[Header("过渡")]
|
||||
[SerializeField]
|
||||
[Range(0.001f, 1f)]
|
||||
[Tooltip("渐变持续时间")]
|
||||
private float m_FadeDuration = 0.1f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开属性
|
||||
|
||||
public StyleField Normal => m_Normal;
|
||||
public StyleField Highlighted => m_Highlighted;
|
||||
public StyleField Pressed => m_Pressed;
|
||||
public StyleField Selected => m_Selected;
|
||||
public StyleField Disabled => m_Disabled;
|
||||
|
||||
public float FadeDuration
|
||||
{
|
||||
get => m_FadeDuration;
|
||||
set => m_FadeDuration = value;
|
||||
}
|
||||
|
||||
/// <summary>所有 StyleField</summary>
|
||||
public StyleField[] AllFields => new[]
|
||||
{
|
||||
m_Normal, m_Highlighted, m_Pressed, m_Selected, m_Disabled
|
||||
};
|
||||
|
||||
/// <summary>默认实例</summary>
|
||||
public static StyleSheetState Default => new StyleSheetState();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 查找
|
||||
|
||||
/// <summary>根据交互状态获取对应的 StyleField</summary>
|
||||
public StyleField GetForState(StyleSheetSelectionState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case StyleSheetSelectionState.Normal: return m_Normal;
|
||||
case StyleSheetSelectionState.Highlighted: return m_Highlighted;
|
||||
case StyleSheetSelectionState.Pressed: return m_Pressed;
|
||||
case StyleSheetSelectionState.Selected: return m_Selected;
|
||||
case StyleSheetSelectionState.Disabled: return m_Disabled;
|
||||
default: return m_Normal;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 注册 / 注销
|
||||
|
||||
public void RegisterAll()
|
||||
{
|
||||
foreach (var f in AllFields) f?.Register();
|
||||
}
|
||||
|
||||
public void UnregisterAll()
|
||||
{
|
||||
foreach (var f in AllFields) f?.Unregister();
|
||||
}
|
||||
|
||||
public void SubscribeAll(Action<StyleValue> callback)
|
||||
{
|
||||
foreach (var f in AllFields) { if (f != null) f.OnValueChanged += callback; }
|
||||
}
|
||||
|
||||
public void UnsubscribeAll(Action<StyleValue> callback)
|
||||
{
|
||||
foreach (var f in AllFields) { if (f != null) f.OnValueChanged -= callback; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f8a47a476054d142a8987d093216e74
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -4,7 +4,9 @@
|
||||
"references": [
|
||||
"UnityEngine.UI",
|
||||
"Unity.TextMeshPro",
|
||||
"Lrss3.Deconstruction"
|
||||
"Lrss3.Deconstruction",
|
||||
"Unity.Burst",
|
||||
"Unity.Collections"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using TMPro;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using XericUI.Helper;
|
||||
@@ -168,11 +168,11 @@ namespace XericUI.OverrideUI
|
||||
|
||||
#region 组件替换
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To XericButton", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric Button", true)]
|
||||
private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.context is Button and not XericButton;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To XericButton", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric Button", false, 10)]
|
||||
private static void ReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Button, XericButton,
|
||||
(Graphic targetGraphic, ButtonClickedEvent onClick)>(
|
||||
@@ -184,11 +184,11 @@ namespace XericUI.OverrideUI
|
||||
t.onClick = d.onClick;
|
||||
});
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To Button", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericButton;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To Button", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericButton, Button,
|
||||
(Graphic targetGraphic, ButtonClickedEvent onClick)>(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UI;
|
||||
@@ -27,11 +27,11 @@ namespace XericUI.OverrideUI
|
||||
|
||||
#region 组件替换
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To XericImage", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric Image", true)]
|
||||
private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.context is Image and not XericImage;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To XericImage", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric Image", false, 10)]
|
||||
private static void ReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Image, XericImage, Sprite>(
|
||||
o =>
|
||||
@@ -41,11 +41,11 @@ namespace XericUI.OverrideUI
|
||||
t.sprite = d;
|
||||
});
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To Image", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericImage;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To Image", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericImage, Image, Sprite>(
|
||||
o =>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
using XericUI.Core.StyleSheetUI;
|
||||
using XericUI.Helper;
|
||||
|
||||
namespace XericUI.OverrideUI
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于 SuperStyleSheet 的按钮组件。
|
||||
/// 通过两个独立的 StyleSheetState 实例分别管理颜色和纹理的五态样式切换,
|
||||
/// 替代 Unity 内置的 ColorBlock 和 SpriteState。
|
||||
/// </summary>
|
||||
[AddComponentMenu("Xeric UI Vessel/UI/Style Button", 51)]
|
||||
public class XericStyleButton : Button
|
||||
{
|
||||
#region 序列化字段
|
||||
|
||||
[Header("样式表状态")]
|
||||
[SerializeField]
|
||||
[Tooltip("五态颜色样式块")]
|
||||
private StyleSheetState m_ColorState = StyleSheetState.Default;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("五态纹理样式块")]
|
||||
private StyleSheetState m_TextureState = StyleSheetState.Default;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("启用样式表驱动过渡(关闭则回退为 Unity ColorBlock)")]
|
||||
private bool m_EnableStyleTransition = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 运行时
|
||||
|
||||
[System.NonSerialized] private StyleSheetSelectionState m_CurrentState = StyleSheetSelectionState.Normal;
|
||||
[System.NonSerialized] private Graphic m_TargetGraphic;
|
||||
[System.NonSerialized] private Image m_TargetImage;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
m_TargetGraphic = targetGraphic;
|
||||
m_TargetImage = m_TargetGraphic as Image;
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
|
||||
m_ColorState?.RegisterAll();
|
||||
m_TextureState?.RegisterAll();
|
||||
m_ColorState?.SubscribeAll(OnStyleChanged);
|
||||
m_TextureState?.SubscribeAll(OnStyleChanged);
|
||||
|
||||
ApplyForState(m_CurrentState);
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
m_ColorState?.UnsubscribeAll(OnStyleChanged);
|
||||
m_TextureState?.UnsubscribeAll(OnStyleChanged);
|
||||
m_ColorState?.UnregisterAll();
|
||||
m_TextureState?.UnregisterAll();
|
||||
|
||||
base.OnDisable();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 状态过渡
|
||||
|
||||
protected override void DoStateTransition(SelectionState state, bool instant)
|
||||
{
|
||||
if (!gameObject.activeInHierarchy) return;
|
||||
|
||||
m_CurrentState = (StyleSheetSelectionState)(int)state;
|
||||
|
||||
if (m_EnableStyleTransition)
|
||||
ApplyForState(m_CurrentState, instant);
|
||||
else
|
||||
base.DoStateTransition(state, instant);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 样式应用
|
||||
|
||||
private void ApplyForState(StyleSheetSelectionState state, bool instant = false)
|
||||
{
|
||||
// 颜色
|
||||
if (m_ColorState != null)
|
||||
{
|
||||
StyleField colorField = m_ColorState.GetForState(state);
|
||||
if (colorField != null && m_TargetGraphic != null)
|
||||
{
|
||||
StyleValue val = colorField.GetValue();
|
||||
if (val != null)
|
||||
m_TargetGraphic.CrossFadeColor((Color)val,
|
||||
instant ? 0f : m_ColorState.FadeDuration, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 纹理
|
||||
if (m_TextureState != null && m_TargetImage != null)
|
||||
{
|
||||
StyleField texField = m_TextureState.GetForState(state);
|
||||
if (texField != null)
|
||||
{
|
||||
StyleValue val = texField.GetValue();
|
||||
Texture2D tex = val?.GetValueAs<Texture2D>();
|
||||
if (tex != null)
|
||||
m_TargetImage.overrideSprite = Sprite.Create(tex,
|
||||
new Rect(0, 0, tex.width, tex.height),
|
||||
new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStyleChanged(StyleValue _)
|
||||
{
|
||||
if (gameObject.activeInHierarchy)
|
||||
ApplyForState(m_CurrentState);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric (Style)", true)]
|
||||
private static bool ValidateReplaceToStyleButton(UnityEditor.MenuCommand command)
|
||||
=> command.context is Button and not XericStyleButton;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric (Style)", false, 11)]
|
||||
private static void ReplaceToStyleButton(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Button, XericStyleButton,
|
||||
(Graphic targetGraphic, ButtonClickedEvent onClick, Navigation navigation)>(
|
||||
o => (o.targetGraphic, o.onClick, o.navigation),
|
||||
(t, d) =>
|
||||
{
|
||||
t.targetGraphic = d.targetGraphic;
|
||||
t.onClick = d.onClick;
|
||||
t.navigation = d.navigation;
|
||||
});
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", true)]
|
||||
private static bool ValidateReplaceStyleToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericStyleButton;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", false, 100)]
|
||||
private static void ReplaceStyleToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericStyleButton, Button,
|
||||
(Graphic targetGraphic, ButtonClickedEvent onClick, Navigation navigation)>(
|
||||
o => (o.targetGraphic, o.onClick, o.navigation),
|
||||
(t, d) =>
|
||||
{
|
||||
t.targetGraphic = d.targetGraphic;
|
||||
t.onClick = d.onClick;
|
||||
t.navigation = d.navigation;
|
||||
});
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac135f8a0c9ac114aabb20e033ab1331
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,140 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
using XericUI.Helper;
|
||||
|
||||
namespace XericUI.OverrideUI
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于 SuperStyleSheet 的图像组件。
|
||||
/// 使用 StyleField 引用样式表中的颜色和纹理路径,样式变更时自动刷新。
|
||||
/// </summary>
|
||||
[AddComponentMenu("Xeric UI Vessel/UI/Style Image", 53)]
|
||||
public class XericStyleImage : Image
|
||||
{
|
||||
#region 序列化字段
|
||||
|
||||
[Header("图像样式")]
|
||||
[SerializeField]
|
||||
[Tooltip("颜色样式路径")]
|
||||
private StyleField m_ColorStyle = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("纹理/精灵样式路径")]
|
||||
private StyleField m_TextureStyle = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("材质样式路径")]
|
||||
private StyleField m_MaterialStyle = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("启用样式表自动刷新")]
|
||||
private bool m_AutoRefresh = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 运行时
|
||||
|
||||
[System.NonSerialized] private Sprite m_ResolvedSprite;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
|
||||
if (!m_AutoRefresh) return;
|
||||
|
||||
m_ColorStyle?.Register();
|
||||
m_TextureStyle?.Register();
|
||||
m_MaterialStyle?.Register();
|
||||
m_ColorStyle.OnValueChanged += OnStyleChanged;
|
||||
m_TextureStyle.OnValueChanged += OnStyleChanged;
|
||||
m_MaterialStyle.OnValueChanged += OnStyleChanged;
|
||||
|
||||
ApplyAllStyles();
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
if (m_ColorStyle != null) { m_ColorStyle.OnValueChanged -= OnStyleChanged; m_ColorStyle.Unregister(); }
|
||||
if (m_TextureStyle != null) { m_TextureStyle.OnValueChanged -= OnStyleChanged; m_TextureStyle.Unregister(); }
|
||||
if (m_MaterialStyle != null) { m_MaterialStyle.OnValueChanged -= OnStyleChanged; m_MaterialStyle.Unregister(); }
|
||||
|
||||
base.OnDisable();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 样式应用
|
||||
|
||||
public void ApplyAllStyles()
|
||||
{
|
||||
if (m_ColorStyle != null)
|
||||
{
|
||||
StyleValue val = m_ColorStyle.GetValue();
|
||||
if (val != null) color = val;
|
||||
}
|
||||
|
||||
if (m_TextureStyle != null)
|
||||
{
|
||||
StyleValue val = m_TextureStyle.GetValue();
|
||||
Sprite sp = val?.GetValueAs<Sprite>();
|
||||
if (sp != null) { m_ResolvedSprite = sp; sprite = sp; }
|
||||
else
|
||||
{
|
||||
Texture2D tex = val?.GetValueAs<Texture2D>();
|
||||
if (tex != null)
|
||||
{
|
||||
m_ResolvedSprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f));
|
||||
sprite = m_ResolvedSprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_MaterialStyle != null)
|
||||
{
|
||||
StyleValue val = m_MaterialStyle.GetValue();
|
||||
Material mat = val?.GetValueAs<Material>();
|
||||
if (mat != null) material = mat;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStyleChanged(StyleValue val)
|
||||
{
|
||||
if (gameObject.activeInHierarchy)
|
||||
ApplyAllStyles();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric (Style)", true)]
|
||||
private static bool ValidateReplaceToStyleImage(UnityEditor.MenuCommand command)
|
||||
=> command.context is Image and not XericStyleImage;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric (Style)", false, 11)]
|
||||
private static void ReplaceToStyleImage(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Image, XericStyleImage, Sprite>(
|
||||
o => o.sprite,
|
||||
(t, d) => { t.sprite = d; });
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", true)]
|
||||
private static bool ValidateReplaceStyleImageToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericStyleImage;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", false, 100)]
|
||||
private static void ReplaceStyleImageToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericStyleImage, Image, Sprite>(
|
||||
o => o.sprite,
|
||||
(t, d) => { t.sprite = d; });
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dff7107391ce75149acdb57d775d7d3c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,209 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
using XericUI.Core.StyleSheetUI;
|
||||
using XericUI.Helper;
|
||||
|
||||
namespace XericUI.OverrideUI
|
||||
{
|
||||
/// <summary>
|
||||
/// 基于 SuperStyleSheet 的开关组件。
|
||||
/// 使用两个 StyleSheetState 实例管理五态颜色和纹理样式,
|
||||
/// 额外支持 isOn 覆盖和 Checkmark 样式。
|
||||
/// </summary>
|
||||
[AddComponentMenu("Xeric UI Vessel/UI/Style Toggle", 52)]
|
||||
public class XericStyleToggle : Toggle
|
||||
{
|
||||
#region 序列化字段
|
||||
|
||||
[Header("样式表状态")]
|
||||
[SerializeField]
|
||||
private StyleSheetState m_ColorState = StyleSheetState.Default;
|
||||
|
||||
[SerializeField]
|
||||
private StyleSheetState m_TextureState = StyleSheetState.Default;
|
||||
|
||||
[Header("Toggle 专用")]
|
||||
[SerializeField]
|
||||
[Tooltip("isOn=true 时的颜色覆盖")]
|
||||
private StyleField m_OnOverrideColor = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("isOn=true 时的纹理覆盖")]
|
||||
private StyleField m_OnOverrideTexture = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Checkmark 打勾标志的样式")]
|
||||
private StyleField m_CheckmarkStyle = new StyleField();
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("启用样式表驱动过渡")]
|
||||
private bool m_EnableStyleTransition = true;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 运行时
|
||||
|
||||
[System.NonSerialized] private StyleSheetSelectionState m_CurrentState = StyleSheetSelectionState.Normal;
|
||||
[System.NonSerialized] private Graphic m_TargetGraphic;
|
||||
[System.NonSerialized] private Image m_TargetImage;
|
||||
[System.NonSerialized] private Graphic m_CheckmarkGraphic;
|
||||
[System.NonSerialized] private Image m_CheckmarkImage;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
m_TargetGraphic = targetGraphic;
|
||||
m_TargetImage = m_TargetGraphic as Image;
|
||||
|
||||
Transform checkmark = transform.Find("Background/Checkmark")
|
||||
?? transform.Find("Checkmark");
|
||||
if (checkmark != null)
|
||||
{
|
||||
m_CheckmarkGraphic = checkmark.GetComponent<Graphic>();
|
||||
m_CheckmarkImage = checkmark.GetComponent<Image>();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
|
||||
m_ColorState?.RegisterAll();
|
||||
m_TextureState?.RegisterAll();
|
||||
m_ColorState?.SubscribeAll(OnStyleChanged);
|
||||
m_TextureState?.SubscribeAll(OnStyleChanged);
|
||||
|
||||
m_OnOverrideColor?.Register();
|
||||
m_OnOverrideColor.OnValueChanged += OnStyleChanged;
|
||||
m_OnOverrideTexture?.Register();
|
||||
m_OnOverrideTexture.OnValueChanged += OnStyleChanged;
|
||||
m_CheckmarkStyle?.Register();
|
||||
m_CheckmarkStyle.OnValueChanged += OnStyleChanged;
|
||||
|
||||
onValueChanged.AddListener(OnToggleValueChanged);
|
||||
ApplyForState(m_CurrentState);
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
onValueChanged.RemoveListener(OnToggleValueChanged);
|
||||
|
||||
m_ColorState?.UnsubscribeAll(OnStyleChanged);
|
||||
m_TextureState?.UnsubscribeAll(OnStyleChanged);
|
||||
m_ColorState?.UnregisterAll();
|
||||
m_TextureState?.UnregisterAll();
|
||||
if (m_OnOverrideColor != null) { m_OnOverrideColor.OnValueChanged -= OnStyleChanged; m_OnOverrideColor.Unregister(); }
|
||||
if (m_OnOverrideTexture != null) { m_OnOverrideTexture.OnValueChanged -= OnStyleChanged; m_OnOverrideTexture.Unregister(); }
|
||||
if (m_CheckmarkStyle != null) { m_CheckmarkStyle.OnValueChanged -= OnStyleChanged; m_CheckmarkStyle.Unregister(); }
|
||||
|
||||
base.OnDisable();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 状态过渡
|
||||
|
||||
protected override void DoStateTransition(SelectionState state, bool instant)
|
||||
{
|
||||
if (!gameObject.activeInHierarchy) return;
|
||||
m_CurrentState = (StyleSheetSelectionState)(int)state;
|
||||
|
||||
if (m_EnableStyleTransition)
|
||||
ApplyForState(m_CurrentState, instant);
|
||||
else
|
||||
base.DoStateTransition(state, instant);
|
||||
}
|
||||
|
||||
private void OnToggleValueChanged(bool _)
|
||||
{
|
||||
if (m_EnableStyleTransition)
|
||||
ApplyForState(m_CurrentState);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 样式应用
|
||||
|
||||
private void ApplyForState(StyleSheetSelectionState state, bool instant = false)
|
||||
{
|
||||
// 颜色:On 覆盖优先
|
||||
StyleField colorField = isOn && m_OnOverrideColor?.GetValue() != null
|
||||
? m_OnOverrideColor
|
||||
: m_ColorState?.GetForState(state);
|
||||
|
||||
if (colorField != null && m_TargetGraphic != null)
|
||||
{
|
||||
StyleValue val = colorField.GetValue();
|
||||
if (val != null)
|
||||
{
|
||||
float dur = instant ? 0f : (m_ColorState?.FadeDuration ?? 0.1f);
|
||||
m_TargetGraphic.CrossFadeColor((Color)val, dur, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 纹理:On 覆盖优先
|
||||
StyleField texField = isOn && m_OnOverrideTexture?.GetValue() != null
|
||||
? m_OnOverrideTexture
|
||||
: m_TextureState?.GetForState(state);
|
||||
|
||||
if (texField != null && m_TargetImage != null)
|
||||
{
|
||||
StyleValue val = texField.GetValue();
|
||||
Texture2D tex = val?.GetValueAs<Texture2D>();
|
||||
if (tex != null)
|
||||
m_TargetImage.overrideSprite = Sprite.Create(tex,
|
||||
new Rect(0, 0, tex.width, tex.height),
|
||||
new Vector2(0.5f, 0.5f));
|
||||
}
|
||||
|
||||
// Checkmark
|
||||
if (m_CheckmarkGraphic != null && m_CheckmarkStyle != null)
|
||||
{
|
||||
StyleValue val = m_CheckmarkStyle.GetValue();
|
||||
if (val != null) m_CheckmarkGraphic.color = val;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStyleChanged(StyleValue _)
|
||||
{
|
||||
if (gameObject.activeInHierarchy)
|
||||
ApplyForState(m_CurrentState);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric (Style)", true)]
|
||||
private static bool ValidateReplaceToStyleToggle(UnityEditor.MenuCommand command)
|
||||
=> command.context is Toggle and not XericStyleToggle;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric (Style)", false, 11)]
|
||||
private static void ReplaceToStyleToggle(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Toggle, XericStyleToggle,
|
||||
(Graphic graphic, ToggleGroup group, ToggleEvent onValueChanged, Navigation navigation)>(
|
||||
o => (o.graphic, o.group, o.onValueChanged, o.navigation),
|
||||
(t, d) => { t.graphic = d.graphic; t.group = d.group; t.onValueChanged = d.onValueChanged; t.navigation = d.navigation; });
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", true)]
|
||||
private static bool ValidateReplaceStyleToggleToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericStyleToggle;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", false, 100)]
|
||||
private static void ReplaceStyleToggleToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericStyleToggle, Toggle,
|
||||
(Graphic graphic, ToggleGroup group, ToggleEvent onValueChanged, Navigation navigation)>(
|
||||
o => (o.graphic, o.group, o.onValueChanged, o.navigation),
|
||||
(t, d) => { t.graphic = d.graphic; t.group = d.group; t.onValueChanged = d.onValueChanged; t.navigation = d.navigation; });
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4002f96b2337614f96d13d1beeb2542
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -89,7 +89,7 @@ namespace XericUI
|
||||
m_ximage = graphic.GetComponent<XericImage>();
|
||||
if (m_ximage)
|
||||
return m_ximage;
|
||||
Debug.Log("注意替换toggle的checkmark为XericImage以支持完整的功能");
|
||||
Debug.Log("ע���滻toggle��checkmarkΪXericImage��֧�������Ĺ���");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -226,13 +226,13 @@ namespace XericUI
|
||||
#endif
|
||||
}
|
||||
|
||||
#region 组件替换
|
||||
#region ����滻
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To XericToggle", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric Toggle", true)]
|
||||
private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.context is Toggle and not XericToggle;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To XericToggle", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric Toggle", false, 10)]
|
||||
private static void ReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
{
|
||||
command.ReplaceCommandContext<Toggle, XericToggle,
|
||||
@@ -245,14 +245,14 @@ namespace XericUI
|
||||
t.group = d.group;
|
||||
t.onValueChanged = d.onValueChanged;
|
||||
});
|
||||
Debug.Log("请手动替换toggle下的Checkmark Image组件为XericImage,否则Toggle工作时取消选择的状态会保持为ison状态。");
|
||||
Debug.Log("���ֶ��滻toggle�µ�Checkmark Image���ΪXericImage������Toggle����ʱȡ��ѡ���״̬�ᱣ��Ϊison״̬��");
|
||||
}
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To Toggle", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericToggle;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To Toggle", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericToggle, Toggle,
|
||||
(Graphic graphic, ToggleGroup group, ToggleEvent onValueChanged)>(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using XericUI.Helper;
|
||||
@@ -12,19 +12,19 @@ namespace XericUI.OverrideUI
|
||||
|
||||
#region 组件替换
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To XericToggleGroup", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To/Xeric ToggleGroup", true)]
|
||||
private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.context is ToggleGroup and not XericToggleGroup;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To XericToggleGroup", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To/Xeric ToggleGroup", false, 10)]
|
||||
private static void ReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<ToggleGroup, XericToggleGroup>();
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To ToggleGroup", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To/Unity ToggleGroup", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericToggleGroup;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To ToggleGroup", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/ToggleGroup/Replace To/Unity ToggleGroup", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericToggleGroup, ToggleGroup>();
|
||||
#endif
|
||||
|
||||
@@ -27,22 +27,22 @@ namespace XericUI.OverrideUI
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To XericUIrAttributeButton", true)]
|
||||
private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric (Attribute)", true)]
|
||||
private static bool ValidateReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.context is Button and not XericUIrAttributeButton;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To XericUIrAttributeButton", false, 11)]
|
||||
private static void ReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Xeric (Attribute)", false, 12)]
|
||||
private static void ReplaceToXeric(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Button, XericUIrAttributeButton,
|
||||
(Graphic targetGraphic, Button.ButtonClickedEvent onClick)>(
|
||||
o => (o.targetGraphic, o.onClick),
|
||||
(t, d) => { t.targetGraphic = d.targetGraphic; t.onClick = d.onClick; });
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To Button", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericUIrAttributeButton;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To Button", false, 11)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Button/Replace To/Unity Button", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericUIrAttributeButton, Button,
|
||||
(Graphic targetGraphic, Button.ButtonClickedEvent onClick)>(
|
||||
|
||||
@@ -27,11 +27,11 @@ namespace XericUI.OverrideUI
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To XericUIrAttributeDropdown", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To/Xeric (Attribute)", true)]
|
||||
private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.context is Dropdown and not XericUIrAttributeDropdown;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To XericUIrAttributeDropdown", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To/Xeric (Attribute)", false, 10)]
|
||||
private static void ReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Dropdown, XericUIrAttributeDropdown,
|
||||
(Graphic targetGraphic, int value, List<OptionData> options, DropdownEvent onValueChanged)>(
|
||||
@@ -45,11 +45,11 @@ namespace XericUI.OverrideUI
|
||||
t.onValueChanged = d.onValueChanged;
|
||||
});
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To Dropdown", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To/Unity Dropdown", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericUIrAttributeDropdown;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To Dropdown", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Dropdown/Replace To/Unity Dropdown", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericUIrAttributeDropdown, Dropdown,
|
||||
(Graphic targetGraphic, int value, List<OptionData> options, DropdownEvent onValueChanged)>(
|
||||
|
||||
@@ -26,11 +26,11 @@ namespace XericUI.OverrideUI
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/InputField/Replace To XericUIrAttributeInputField", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/InputField/Replace To/Xeric (Attribute)", true)]
|
||||
private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.context is InputField and not XericUIrAttributeInputField;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/InputField/Replace To XericUIrAttributeInputField", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/InputField/Replace To/Xeric (Attribute)", false, 10)]
|
||||
private static void ReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<InputField, XericUIrAttributeInputField,
|
||||
(Graphic targetGraphic, string text, InputField.OnChangeEvent onValueChanged, InputField.EndEditEvent onEndEdit)>(
|
||||
@@ -43,11 +43,11 @@ namespace XericUI.OverrideUI
|
||||
t.onEndEdit = d.onEndEdit;
|
||||
});
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/InputField/Replace To InputField", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/InputField/Replace To/Unity InputField", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericUIrAttributeInputField;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/InputField/Replace To InputField", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/InputField/Replace To/Unity InputField", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericUIrAttributeInputField, InputField,
|
||||
(Graphic targetGraphic, string text, InputField.OnChangeEvent onValueChanged, InputField.EndEditEvent onEndEdit)>(
|
||||
|
||||
@@ -26,11 +26,11 @@ namespace XericUI.OverrideUI
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Slider/Replace To XericUIrAttributeSlider", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Slider/Replace To/Xeric (Attribute)", true)]
|
||||
private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.context is Slider and not XericUIrAttributeSlider;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Slider/Replace To XericUIrAttributeSlider", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Slider/Replace To/Xeric (Attribute)", false, 10)]
|
||||
private static void ReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Slider, XericUIrAttributeSlider,
|
||||
(Graphic targetGraphic, float value, Slider.SliderEvent onValueChanged)>(
|
||||
@@ -43,11 +43,11 @@ namespace XericUI.OverrideUI
|
||||
t.onValueChanged = d.onValueChanged;
|
||||
});
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Slider/Replace To Slider", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Slider/Replace To/Unity Slider", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericUIrAttributeSlider;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Slider/Replace To Slider", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Slider/Replace To/Unity Slider", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericUIrAttributeSlider, Slider,
|
||||
(Graphic targetGraphic, float value, Slider.SliderEvent onValueChanged)>(
|
||||
|
||||
@@ -45,21 +45,21 @@ namespace XericUI.OverrideUI
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To XericUIrAttributeText", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric (AttributeText)", true)]
|
||||
private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.context is Image and not XericUIrAttributeText;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To XericUIrAttributeText", false, 11)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Xeric (AttributeText)", false, 13)]
|
||||
private static void ReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Image, XericUIrAttributeText, Sprite>(
|
||||
o => o.sprite,
|
||||
(t, d) => { t.sprite = d; });
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To Image", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericUIrAttributeText;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To Image", false, 11)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Image/Replace To/Unity Image", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericUIrAttributeText, Image, Sprite>(
|
||||
o => o.sprite,
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace XericUI.OverrideUI
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To XericUIrAttributeTextMeshProInputField", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To/Xeric (Attribute)", true)]
|
||||
private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
{
|
||||
#if dUI_TextMeshPro
|
||||
@@ -69,7 +69,7 @@ namespace XericUI.OverrideUI
|
||||
#endif
|
||||
}
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To XericUIrAttributeTextMeshProInputField", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To/Xeric (Attribute)", false, 10)]
|
||||
private static void ReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
{
|
||||
#if dUI_TextMeshPro
|
||||
@@ -79,11 +79,11 @@ namespace XericUI.OverrideUI
|
||||
#endif
|
||||
}
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To TMP_InputField", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To/Unity TMP_InputField", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericUIrAttributeTextMeshProInputField;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To TMP_InputField", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/TMP_InputField/Replace To/Unity TMP_InputField", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
{
|
||||
var comp = command.context as XericUIrAttributeTextMeshProInputField;
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace XericUI.OverrideUI
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To XericUIrAttributeTextMeshProText", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To/Xeric (Attribute)", true)]
|
||||
private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
{
|
||||
#if dUI_TextMeshPro
|
||||
@@ -66,7 +66,7 @@ namespace XericUI.OverrideUI
|
||||
#endif
|
||||
}
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To XericUIrAttributeTextMeshProText", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To/Xeric (Attribute)", false, 10)]
|
||||
private static void ReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
{
|
||||
#if dUI_TextMeshPro
|
||||
@@ -76,11 +76,11 @@ namespace XericUI.OverrideUI
|
||||
#endif
|
||||
}
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To TextMeshProUGUI", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To/Unity TextMeshPro", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericUIrAttributeTextMeshProText;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To TextMeshProUGUI", false, 10)]
|
||||
[UnityEditor.MenuItem("CONTEXT/TextMeshProUGUI/Replace To/Unity TextMeshPro", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
{
|
||||
var comp = command.context as XericUIrAttributeTextMeshProText;
|
||||
|
||||
@@ -26,11 +26,11 @@ namespace XericUI.OverrideUI
|
||||
#region 组件替换
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To XericUIrAttributeToggle", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric (Attribute)", true)]
|
||||
private static bool ValidateReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.context is Toggle and not XericUIrAttributeToggle;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To XericUIrAttributeToggle", false, 11)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Xeric (Attribute)", false, 12)]
|
||||
private static void ReplaceToAttribute(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<Toggle, XericUIrAttributeToggle,
|
||||
(Graphic graphic, ToggleGroup group, Toggle.ToggleEvent onValueChanged)>(
|
||||
@@ -42,11 +42,11 @@ namespace XericUI.OverrideUI
|
||||
t.onValueChanged = d.onValueChanged;
|
||||
});
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To Toggle", true)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", true)]
|
||||
private static bool ValidateReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.context is XericUIrAttributeToggle;
|
||||
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To Toggle", false, 11)]
|
||||
[UnityEditor.MenuItem("CONTEXT/Toggle/Replace To/Unity Toggle", false, 100)]
|
||||
private static void ReplaceToUnity(UnityEditor.MenuCommand command)
|
||||
=> command.ReplaceCommandContext<XericUIrAttributeToggle, Toggle,
|
||||
(Graphic graphic, ToggleGroup group, Toggle.ToggleEvent onValueChanged)>(
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 758008eeefb9f0540b0dce989b56c968
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 186fffef629f713499774b802be4f587
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XericUI.XTable.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个数据块——保存 BlockSizeX × BlockSizeY 个单元格数据。
|
||||
/// 数据使用线性数组存储,索引 = localRow * BlockSizeX + localCol。
|
||||
/// </summary>
|
||||
public class XTableBlock
|
||||
{
|
||||
/// <summary>块在全局块坐标系中的行号</summary>
|
||||
public int BlockRow;
|
||||
|
||||
/// <summary>块在全局块坐标系中的列号</summary>
|
||||
public int BlockCol;
|
||||
|
||||
/// <summary>块内单元格列数</summary>
|
||||
public int BlockSizeX;
|
||||
|
||||
/// <summary>块内单元格行数</summary>
|
||||
public int BlockSizeY;
|
||||
|
||||
/// <summary>
|
||||
/// 数据存储——线性数组。
|
||||
/// 索引 = localRow * BlockSizeX + localCol。
|
||||
/// </summary>
|
||||
public XTableCellData[] Cells;
|
||||
|
||||
/// <summary>合并单元格描述列表</summary>
|
||||
public List<XTableMergeDescriptor> MergeDescriptors;
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定尺寸的块
|
||||
/// </summary>
|
||||
public XTableBlock(int blockSizeX, int blockSizeY)
|
||||
{
|
||||
BlockSizeX = blockSizeX;
|
||||
BlockSizeY = blockSizeY;
|
||||
Cells = new XTableCellData[blockSizeX * blockSizeY];
|
||||
MergeDescriptors = new List<XTableMergeDescriptor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建指定尺寸和位置的块
|
||||
/// </summary>
|
||||
public XTableBlock(int blockSizeX, int blockSizeY, int blockRow, int blockCol)
|
||||
: this(blockSizeX, blockSizeY)
|
||||
{
|
||||
BlockRow = blockRow;
|
||||
BlockCol = blockCol;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过块内局部坐标获取单元格数据
|
||||
/// </summary>
|
||||
public XTableCellData GetCell(int localRow, int localCol)
|
||||
{
|
||||
int index = localRow * BlockSizeX + localCol;
|
||||
if (index < 0 || index >= Cells.Length) return null;
|
||||
return Cells[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过块内局部坐标设置单元格数据
|
||||
/// </summary>
|
||||
public void SetCell(int localRow, int localCol, XTableCellData data)
|
||||
{
|
||||
int index = localRow * BlockSizeX + localCol;
|
||||
if (index < 0 || index >= Cells.Length) return;
|
||||
Cells[index] = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否有合并描述重定向此单元格
|
||||
/// </summary>
|
||||
/// <returns>合并描述,若无重定向则返回 null</returns>
|
||||
public XTableMergeDescriptor GetMergeRedirect(int localRow, int localCol)
|
||||
{
|
||||
if (MergeDescriptors == null || MergeDescriptors.Count == 0)
|
||||
return null;
|
||||
|
||||
for (int i = 0; i < MergeDescriptors.Count; i++)
|
||||
{
|
||||
if (MergeDescriptors[i].Contains(localRow, localCol))
|
||||
return MergeDescriptors[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa9a14d1a32e7fe4f95c29a0c18deddd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.XTable.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 单元格数据模型——仅保存数据内容,不保存自身尺寸。
|
||||
/// 尺寸由渲染阶段的行列标题(行高/列宽)决定。
|
||||
/// 样式通过 StyleNamespace 字符串引用 StyleManager 中的命名空间。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class XTableCellData
|
||||
{
|
||||
/// <summary>文本内容</summary>
|
||||
public string Text;
|
||||
|
||||
/// <summary>图片纹理</summary>
|
||||
public Texture2D Image;
|
||||
|
||||
/// <summary>预制体对象(用于嵌入复杂 UI 元素)</summary>
|
||||
public GameObject Prefab;
|
||||
|
||||
/// <summary>
|
||||
/// 样式命名空间——对应 StyleManager 中的命名空间。
|
||||
/// 为空或 "xeric_table_default" 时使用默认表格样式。
|
||||
/// 渲染时从此命名空间读取 /cell/bg/color、/cell/font/size 等路径。
|
||||
/// </summary>
|
||||
public string StyleNamespace;
|
||||
|
||||
public XTableCellData() { }
|
||||
|
||||
public XTableCellData(string text)
|
||||
{
|
||||
Text = text;
|
||||
}
|
||||
|
||||
public XTableCellData(string text, string styleNamespace = null) : this(text)
|
||||
{
|
||||
StyleNamespace = styleNamespace;
|
||||
}
|
||||
|
||||
public XTableCellData(string text, Texture2D image, string styleNamespace = null) : this(text, styleNamespace)
|
||||
{
|
||||
Image = image;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e783197e3dfc5a5489b99e27504e9cf3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,247 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using XericUI.XTable.Mapping;
|
||||
|
||||
namespace XericUI.XTable.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 表格数据主类——持有块字典,提供单元格级别的读写接口。
|
||||
/// 块按需创建(SetCell 时自动创建不存在的块),通过 Z 曲线索引映射。
|
||||
/// </summary>
|
||||
public class XTableData
|
||||
{
|
||||
#region 属性
|
||||
|
||||
/// <summary>块内列数(默认 32)</summary>
|
||||
public int BlockSizeX { get; private set; }
|
||||
|
||||
/// <summary>块内行数(默认 32)</summary>
|
||||
public int BlockSizeY { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 块字典——key = Z 曲线索引(ulong),value = 数据块。
|
||||
/// 具有哈希表性质,按需创建,键不连续(离散存储)。
|
||||
/// </summary>
|
||||
public Dictionary<ulong, XTableBlock> BlockMap { get; private set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
|
||||
/// <summary>
|
||||
/// 创建表格数据实例
|
||||
/// </summary>
|
||||
/// <param name="blockSizeX">块内列数,默认 32</param>
|
||||
/// <param name="blockSizeY">块内行数,默认 32</param>
|
||||
public XTableData(int blockSizeX = 32, int blockSizeY = 32)
|
||||
{
|
||||
BlockSizeX = blockSizeX > 0 ? blockSizeX : 32;
|
||||
BlockSizeY = blockSizeY > 0 ? blockSizeY : 32;
|
||||
BlockMap = new Dictionary<ulong, XTableBlock>();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 核心访问方法
|
||||
|
||||
/// <summary>
|
||||
/// 通过全局行列坐标获取单元格数据。
|
||||
/// 先计算块坐标和 Z 索引,查找块字典,再检查合并重定向。
|
||||
/// </summary>
|
||||
/// <returns>单元格数据,不存在则返回 null</returns>
|
||||
public XTableCellData GetCell(int row, int col)
|
||||
{
|
||||
XTableCoordinateUtility.CellToBlockCoordinate(
|
||||
row, col, BlockSizeX, BlockSizeY,
|
||||
out int blockRow, out int blockCol,
|
||||
out int localRow, out int localCol);
|
||||
|
||||
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
|
||||
|
||||
if (!BlockMap.TryGetValue(zIndex, out XTableBlock block))
|
||||
return null;
|
||||
|
||||
// 检查合并重定向
|
||||
XTableMergeDescriptor merge = block.GetMergeRedirect(localRow, localCol);
|
||||
if (merge != null)
|
||||
{
|
||||
merge.GetRedirectTarget(out ulong targetBlockIndex, out int targetRow, out int targetCol);
|
||||
|
||||
if (merge.IsCrossBlock)
|
||||
{
|
||||
// 跨块引用:跳转到目标块
|
||||
if (!BlockMap.TryGetValue(targetBlockIndex, out XTableBlock targetBlock))
|
||||
return null;
|
||||
return targetBlock.GetCell(targetRow, targetCol);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 本地引用:同块内重定向
|
||||
return block.GetCell(targetRow, targetCol);
|
||||
}
|
||||
}
|
||||
|
||||
return block.GetCell(localRow, localCol);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取单元格数据
|
||||
/// </summary>
|
||||
public bool TryGetCell(int row, int col, out XTableCellData data)
|
||||
{
|
||||
data = GetCell(row, col);
|
||||
return data != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过全局行列坐标设置单元格数据。
|
||||
/// 如果对应的块不存在,则按需创建。
|
||||
/// </summary>
|
||||
public void SetCell(int row, int col, XTableCellData data)
|
||||
{
|
||||
XTableCoordinateUtility.CellToBlockCoordinate(
|
||||
row, col, BlockSizeX, BlockSizeY,
|
||||
out int blockRow, out int blockCol,
|
||||
out int localRow, out int localCol);
|
||||
|
||||
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
|
||||
|
||||
if (!BlockMap.TryGetValue(zIndex, out XTableBlock block))
|
||||
{
|
||||
block = new XTableBlock(BlockSizeX, BlockSizeY, blockRow, blockCol);
|
||||
BlockMap[zIndex] = block;
|
||||
}
|
||||
|
||||
block.SetCell(localRow, localCol, data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 合并单元格
|
||||
|
||||
/// <summary>
|
||||
/// 合并从 (startRow, startCol) 开始、跨越 rowSpan 行和 colSpan 列的矩形区域。
|
||||
/// 合并源为左上角单元格 (startRow, startCol)。
|
||||
/// 如果合并区域跨越多个块,则每个涉及的块都会创建对应的 MergeDescriptor。
|
||||
/// </summary>
|
||||
public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan)
|
||||
{
|
||||
// 确保合并源单元格存在
|
||||
XTableCoordinateUtility.CellToBlockCoordinate(
|
||||
startRow, startCol, BlockSizeX, BlockSizeY,
|
||||
out int srcBlockRow, out int srcBlockCol,
|
||||
out int srcLocalRow, out int srcLocalCol);
|
||||
|
||||
ulong srcZIndex = XTableBlockMapping.BlockCoordToZIndex(srcBlockRow, srcBlockCol);
|
||||
|
||||
// 遍历合并区域内的所有单元格,分块处理
|
||||
for (int r = startRow; r < startRow + rowSpan; r++)
|
||||
{
|
||||
for (int c = startCol; c < startCol + colSpan; c++)
|
||||
{
|
||||
// 跳过合并源自身
|
||||
if (r == startRow && c == startCol) continue;
|
||||
|
||||
XTableCoordinateUtility.CellToBlockCoordinate(
|
||||
r, c, BlockSizeX, BlockSizeY,
|
||||
out int blockRow, out int blockCol,
|
||||
out int localRow, out int localCol);
|
||||
|
||||
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
|
||||
|
||||
// 确保块存在
|
||||
if (!BlockMap.TryGetValue(zIndex, out XTableBlock block))
|
||||
{
|
||||
block = new XTableBlock(BlockSizeX, BlockSizeY, blockRow, blockCol);
|
||||
BlockMap[zIndex] = block;
|
||||
}
|
||||
|
||||
// 查找或创建此块的合并描述(相同源)
|
||||
XTableMergeDescriptor descriptor = FindOrCreateMergeDescriptor(block,
|
||||
zIndex == srcZIndex ? srcLocalRow : -1,
|
||||
zIndex == srcZIndex ? srcLocalCol : -1,
|
||||
srcZIndex, srcLocalRow, srcLocalCol,
|
||||
zIndex != srcZIndex);
|
||||
|
||||
descriptor.MergedCellSet.Add((localRow, localCol));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查找或创建合并描述
|
||||
/// </summary>
|
||||
private XTableMergeDescriptor FindOrCreateMergeDescriptor(
|
||||
XTableBlock block, int checkLocalRow, int checkLocalCol,
|
||||
ulong srcZIndex, int srcLocalRow, int srcLocalCol, bool isCrossBlock)
|
||||
{
|
||||
// 查找已有描述
|
||||
for (int i = 0; i < block.MergeDescriptors.Count; i++)
|
||||
{
|
||||
var desc = block.MergeDescriptors[i];
|
||||
if (desc.MergeRowSpan > 0 && desc.MergeColSpan > 0)
|
||||
{
|
||||
// 简化的同源判断:跨块引用共享 sourceBlockIndex
|
||||
if (isCrossBlock && desc.IsCrossBlock && desc.SourceBlockIndex == srcZIndex)
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新描述
|
||||
var newDesc = new XTableMergeDescriptor
|
||||
{
|
||||
IsCrossBlock = isCrossBlock,
|
||||
SourceBlockIndex = srcZIndex,
|
||||
SourceLocalRow = srcLocalRow,
|
||||
SourceLocalCol = srcLocalCol
|
||||
};
|
||||
block.MergeDescriptors.Add(newDesc);
|
||||
return newDesc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消指定区域的合并
|
||||
/// </summary>
|
||||
public void UnmergeCells(int startRow, int startCol)
|
||||
{
|
||||
XTableCoordinateUtility.CellToBlockCoordinate(
|
||||
startRow, startCol, BlockSizeX, BlockSizeY,
|
||||
out int blockRow, out int blockCol,
|
||||
out int localRow, out int localCol);
|
||||
|
||||
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
|
||||
|
||||
if (!BlockMap.TryGetValue(zIndex, out XTableBlock block))
|
||||
return;
|
||||
|
||||
// 移除包含此单元格的所有合并描述
|
||||
for (int i = block.MergeDescriptors.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (block.MergeDescriptors[i].Contains(localRow, localCol))
|
||||
{
|
||||
block.MergeDescriptors.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 查询方法
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定块是否存在
|
||||
/// </summary>
|
||||
public bool HasBlock(int blockRow, int blockCol)
|
||||
{
|
||||
ulong zIndex = XTableBlockMapping.BlockCoordToZIndex(blockRow, blockCol);
|
||||
return BlockMap.ContainsKey(zIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取块数量
|
||||
/// </summary>
|
||||
public int BlockCount => BlockMap.Count;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e5d95e8d6d3d144d8fff6e41f175d77
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XericUI.XTable.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 合并单元格描述——所有被合并单元格的 id 指向合并源(左上角单元格)。
|
||||
/// 同一类型支持本地块内引用和跨块引用,通过 <see cref="IsCrossBlock"/> 区分。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class XTableMergeDescriptor
|
||||
{
|
||||
/// <summary>合并区域在本地块内的起始行</summary>
|
||||
public int LocalStartRow;
|
||||
|
||||
/// <summary>合并区域在本地块内的起始列</summary>
|
||||
public int LocalStartCol;
|
||||
|
||||
/// <summary>合并跨越的行数</summary>
|
||||
public int MergeRowSpan;
|
||||
|
||||
/// <summary>合并跨越的列数</summary>
|
||||
public int MergeColSpan;
|
||||
|
||||
/// <summary>是否为跨块引用(true=合并源在另一个块上)</summary>
|
||||
public bool IsCrossBlock;
|
||||
|
||||
/// <summary>
|
||||
/// 跨块引用时:源块在字典中的 key(Z 曲线索引)
|
||||
/// 本地引用时:忽略此字段
|
||||
/// </summary>
|
||||
public ulong SourceBlockIndex;
|
||||
|
||||
/// <summary>源单元格在块内的行号(跨块时=源块的行、本地时=本块的行)</summary>
|
||||
public int SourceLocalRow;
|
||||
|
||||
/// <summary>源单元格在块内的列号(跨块时=源块的列、本地时=本块的列)</summary>
|
||||
public int SourceLocalCol;
|
||||
|
||||
/// <summary>本块内被合并的 (row, col) 集合,查询时快速判断是否需要重定向</summary>
|
||||
public HashSet<(int row, int col)> MergedCellSet;
|
||||
|
||||
public XTableMergeDescriptor()
|
||||
{
|
||||
MergedCellSet = new HashSet<(int, int)>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定本地坐标是否在此合并区域内
|
||||
/// </summary>
|
||||
public bool Contains(int localRow, int localCol)
|
||||
{
|
||||
return MergedCellSet.Contains((localRow, localCol));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取重定向目标——返回 (目标块Z索引, 目标块内行, 目标块内列)
|
||||
/// 本地引用时目标块索引为 0(由调用方忽略)
|
||||
/// </summary>
|
||||
public void GetRedirectTarget(out ulong targetBlockIndex, out int targetRow, out int targetCol)
|
||||
{
|
||||
targetBlockIndex = IsCrossBlock ? SourceBlockIndex : 0;
|
||||
targetRow = SourceLocalRow;
|
||||
targetCol = SourceLocalCol;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abe5a0c5395fa9249a66cd7195a49dbf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac372aa4265804648bcff4f505b39f24
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using XericLibrary.Runtime.MacroLibrary;
|
||||
|
||||
namespace XericUI.XTable.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 块映射工具——将块坐标与 Z 曲线索引进行相互转换。
|
||||
/// 内部调用 <see cref="MacroCurveMapping"/> 的 Morton Code 编码/解码方法。
|
||||
/// Z 曲线索引避免纯横向排列产生的空缺问题,提升空间局部性。
|
||||
/// </summary>
|
||||
public static class XTableBlockMapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 块坐标 → Z 曲线索引 (Morton Code)
|
||||
/// </summary>
|
||||
/// <param name="blockRow">块行号 (非负)</param>
|
||||
/// <param name="blockCol">块列号 (非负)</param>
|
||||
/// <returns>64 位 Z 曲线索引,用作块字典的 key</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ulong BlockCoordToZIndex(int blockRow, int blockCol)
|
||||
{
|
||||
return MacroCurveMapping.ZOrderEncode(blockCol, blockRow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Z 曲线索引 → 块坐标
|
||||
/// </summary>
|
||||
/// <param name="zIndex">Z 曲线索引</param>
|
||||
/// <param name="blockRow">解码后的块行号</param>
|
||||
/// <param name="blockCol">解码后的块列号</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void ZIndexToBlockCoord(ulong zIndex, out int blockRow, out int blockCol)
|
||||
{
|
||||
MacroCurveMapping.ZOrderDecode(zIndex, out blockCol, out blockRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d2cad58624b7724fb43fdeca3aa2b50
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,79 @@
|
||||
#if ENABLE_BURST
|
||||
using Unity.Burst;
|
||||
#endif
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace XericUI.XTable.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// 表格坐标计算工具——提供单元格坐标 ↔ 块坐标 的转换方法。
|
||||
/// 所有方法支持 Burst 编译加速。
|
||||
/// </summary>
|
||||
#if ENABLE_BURST
|
||||
[BurstCompile]
|
||||
#endif
|
||||
public static class XTableCoordinateUtility
|
||||
{
|
||||
/// <summary>
|
||||
/// 将全局单元格坐标 (row, col) 转换为 块坐标 + 块内局部坐标。
|
||||
/// blockRow = row / blockSizeY, blockCol = col / blockSizeX
|
||||
/// localRow = row % blockSizeY, localCol = col % blockSizeX
|
||||
/// </summary>
|
||||
#if ENABLE_BURST
|
||||
[BurstCompile]
|
||||
#endif
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void CellToBlockCoordinate(
|
||||
int row, int col, int blockSizeX, int blockSizeY,
|
||||
out int blockRow, out int blockCol,
|
||||
out int localRow, out int localCol)
|
||||
{
|
||||
blockRow = row / blockSizeY;
|
||||
blockCol = col / blockSizeX;
|
||||
localRow = row % blockSizeY;
|
||||
localCol = col % blockSizeX;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算块内的线性索引。
|
||||
/// index = localRow * blockSizeX + localCol
|
||||
/// </summary>
|
||||
#if ENABLE_BURST
|
||||
[BurstCompile]
|
||||
#endif
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int LocalCellIndex(int localRow, int localCol, int blockSizeX)
|
||||
{
|
||||
return localRow * blockSizeX + localCol;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从块内线性索引反算局部行列坐标。
|
||||
/// </summary>
|
||||
#if ENABLE_BURST
|
||||
[BurstCompile]
|
||||
#endif
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void LocalCellFromIndex(int index, int blockSizeX,
|
||||
out int localRow, out int localCol)
|
||||
{
|
||||
localRow = index / blockSizeX;
|
||||
localCol = index % blockSizeX;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从全局行列坐标直接计算块内线性索引。
|
||||
/// </summary>
|
||||
#if ENABLE_BURST
|
||||
[BurstCompile]
|
||||
#endif
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int GlobalCellToLocalIndex(int row, int col, int blockSizeX, int blockSizeY)
|
||||
{
|
||||
int localRow = row % blockSizeY;
|
||||
int localCol = col % blockSizeX;
|
||||
return localRow * blockSizeX + localCol;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 45c10335c21cdf342816fd78d61cec09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b7ab03cb8f1d894a92f9ef988cb9245
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7813b6f36b85854b8bfcae0874499d1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 脏标记类型——标识表格需要刷新的变更类型。
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum TableDirtyType
|
||||
{
|
||||
None = 0,
|
||||
/// <summary>数据内容变更</summary>
|
||||
DataChanged = 1 << 0,
|
||||
/// <summary>视图范围变更(滚动/缩放)</summary>
|
||||
ViewChanged = 1 << 1,
|
||||
/// <summary>样式表变更</summary>
|
||||
StyleChanged = 1 << 2,
|
||||
/// <summary>选中状态变更</summary>
|
||||
SelectionChanged = 1 << 3,
|
||||
/// <summary>行列尺寸变更</summary>
|
||||
LayoutChanged = 1 << 4,
|
||||
/// <summary>全部脏(强制完整刷新)</summary>
|
||||
All = DataChanged | ViewChanged | StyleChanged | SelectionChanged | LayoutChanged
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 脏标记结构——追踪表格的刷新状态。
|
||||
/// </summary>
|
||||
public struct XTableDirtyFlag
|
||||
{
|
||||
/// <summary>当前脏标记集合</summary>
|
||||
public TableDirtyType Flags;
|
||||
|
||||
/// <summary>是否有任何脏标记</summary>
|
||||
public bool IsDirty => Flags != TableDirtyType.None;
|
||||
|
||||
/// <summary>标记一种脏类型</summary>
|
||||
public void Mark(TableDirtyType type)
|
||||
{
|
||||
Flags |= type;
|
||||
}
|
||||
|
||||
/// <summary>清除所有脏标记</summary>
|
||||
public void Clear()
|
||||
{
|
||||
Flags = TableDirtyType.None;
|
||||
}
|
||||
|
||||
/// <summary>检查是否包含特定脏类型</summary>
|
||||
public bool Has(TableDirtyType type)
|
||||
{
|
||||
return (Flags & type) == type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f87a16b53fa84c4a97b8ffd44aa2868
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 单元格对象池——复用表格单元格 GameObject,避免频繁 Instantiate/Destroy。
|
||||
/// </summary>
|
||||
public class XTableObjectPool
|
||||
{
|
||||
/// <summary>回收池</summary>
|
||||
private readonly Stack<GameObject> m_Pool = new Stack<GameObject>();
|
||||
|
||||
/// <summary>当前活跃对象(已借出)</summary>
|
||||
private readonly HashSet<GameObject> m_Active = new HashSet<GameObject>();
|
||||
|
||||
/// <summary>预制体模板</summary>
|
||||
private readonly GameObject m_Prefab;
|
||||
|
||||
/// <summary>父级 Transform</summary>
|
||||
private readonly Transform m_Parent;
|
||||
|
||||
/// <summary>初始池容量</summary>
|
||||
private readonly int m_InitialCapacity;
|
||||
|
||||
public XTableObjectPool(GameObject prefab, Transform parent, int initialCapacity = 64)
|
||||
{
|
||||
m_Prefab = prefab;
|
||||
m_Parent = parent;
|
||||
m_InitialCapacity = initialCapacity;
|
||||
|
||||
// 预创建对象
|
||||
for (int i = 0; i < initialCapacity; i++)
|
||||
{
|
||||
GameObject obj = CreateNew();
|
||||
obj.SetActive(false);
|
||||
m_Pool.Push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从池中获取一个对象
|
||||
/// </summary>
|
||||
public GameObject Get()
|
||||
{
|
||||
GameObject obj;
|
||||
if (m_Pool.Count > 0)
|
||||
{
|
||||
obj = m_Pool.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
obj = CreateNew();
|
||||
}
|
||||
|
||||
obj.SetActive(true);
|
||||
m_Active.Add(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将对象归还到池中
|
||||
/// </summary>
|
||||
public void Return(GameObject obj)
|
||||
{
|
||||
if (obj == null) return;
|
||||
|
||||
obj.SetActive(false);
|
||||
m_Active.Remove(obj);
|
||||
m_Pool.Push(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 归还所有活跃对象
|
||||
/// </summary>
|
||||
public void ReturnAll()
|
||||
{
|
||||
foreach (var obj in m_Active)
|
||||
{
|
||||
if (obj != null)
|
||||
{
|
||||
obj.SetActive(false);
|
||||
m_Pool.Push(obj);
|
||||
}
|
||||
}
|
||||
m_Active.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理所有对象
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
ReturnAll();
|
||||
while (m_Pool.Count > 0)
|
||||
{
|
||||
var obj = m_Pool.Pop();
|
||||
if (obj != null)
|
||||
Object.Destroy(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>当前活跃对象数</summary>
|
||||
public int ActiveCount => m_Active.Count;
|
||||
|
||||
/// <summary>池中空闲对象数</summary>
|
||||
public int PoolCount => m_Pool.Count;
|
||||
|
||||
private GameObject CreateNew()
|
||||
{
|
||||
GameObject obj = m_Prefab != null
|
||||
? Object.Instantiate(m_Prefab, m_Parent)
|
||||
: new GameObject("TableCell", typeof(RectTransform));
|
||||
|
||||
obj.transform.SetParent(m_Parent, false);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ae4a5754316035e4489de05e40389c9e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,572 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
using TMPro;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericUI.Core.Base;
|
||||
using XericUI.XTable.Core;
|
||||
using XericUI.XTable.Rendering.Elements;
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Component
|
||||
{
|
||||
/// <summary>
|
||||
/// 表格组件——纯数据渲染器。
|
||||
/// 一张全幅背景 + 按需拼装单元格文本/图片。
|
||||
/// 无行列标题、无样式差异分割线——所有内容由数据定义。
|
||||
/// </summary>
|
||||
[ExecuteAlways]
|
||||
[AddComponentMenu("Xeric UI Vessel/Table/Xeric UI Action Table", 60)]
|
||||
public class XericUIActionTable : XericUIBehaciour, IPointerClickHandler
|
||||
{
|
||||
#region 常量
|
||||
|
||||
private const string DEFAULT_STYLE_NS = "xeric_table_default";
|
||||
|
||||
#endregion
|
||||
|
||||
#region 序列化字段
|
||||
|
||||
[SerializeField] private XTableData m_TableData;
|
||||
|
||||
[SerializeField] private float[] m_RowHeights = new float[] { 40f, 30f };
|
||||
|
||||
[SerializeField] private float[] m_ColWidths = new float[] { 100f, 80f, 120f };
|
||||
|
||||
[SerializeField] private string m_DefaultStyleNamespace = DEFAULT_STYLE_NS;
|
||||
|
||||
[SerializeField] private ScrollRect m_ScrollRect;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有字段
|
||||
|
||||
[System.NonSerialized] private CellAssembler m_Assembler;
|
||||
[System.NonSerialized] private XTableDirtyFlag m_DirtyFlag;
|
||||
|
||||
private int m_SelectedRow = -1;
|
||||
private int m_SelectedCol = -1;
|
||||
[System.NonSerialized] private GameObject m_BackgroundGo;
|
||||
|
||||
private float m_TotalWidth;
|
||||
private float m_TotalHeight;
|
||||
|
||||
// ScrollRect 可见范围
|
||||
private Rect m_VisibleRect;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件
|
||||
|
||||
public event Action<int, int> OnCellSelected;
|
||||
public event Action OnTableRefreshed;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开属性
|
||||
|
||||
public XTableData TableData
|
||||
{
|
||||
get => m_TableData;
|
||||
set { m_TableData = value; MarkDirty(TableDirtyType.DataChanged); }
|
||||
}
|
||||
|
||||
public float[] RowHeights => m_RowHeights;
|
||||
public float[] ColWidths => m_ColWidths;
|
||||
|
||||
public string DefaultStyleNamespace => m_DefaultStyleNamespace;
|
||||
public int SelectedRow => m_SelectedRow;
|
||||
public int SelectedCol => m_SelectedCol;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
if (m_TableData == null)
|
||||
m_TableData = new XTableData();
|
||||
|
||||
m_Assembler = new CellAssembler(rectTransform);
|
||||
RecalculateTotalSize();
|
||||
EnsureDefaultStyleNamespace();
|
||||
CreateDefaultSampleData();
|
||||
CreateBackground();
|
||||
|
||||
if (m_ScrollRect != null)
|
||||
m_ScrollRect.onValueChanged.AddListener(OnScrollValueChanged);
|
||||
|
||||
if (!Application.isPlaying)
|
||||
SubscribeEditorUpdate();
|
||||
|
||||
MarkDirty(TableDirtyType.All);
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
EnsureDefaultStyleNamespace();
|
||||
MarkDirty(TableDirtyType.All);
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
if (m_ScrollRect != null)
|
||||
m_ScrollRect.onValueChanged.RemoveListener(OnScrollValueChanged);
|
||||
UnsubscribeEditorUpdate();
|
||||
|
||||
// 销毁所有动态生成的对象
|
||||
if (m_Assembler != null)
|
||||
m_Assembler.DestroyAll();
|
||||
if (m_BackgroundGo != null)
|
||||
{
|
||||
if (Application.isPlaying) Destroy(m_BackgroundGo);
|
||||
else DestroyImmediate(m_BackgroundGo);
|
||||
m_BackgroundGo = null;
|
||||
}
|
||||
|
||||
base.OnDestroy();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (m_DirtyFlag.IsDirty) RefreshView();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 默认示例数据
|
||||
|
||||
private bool m_DefaultDataCreated;
|
||||
|
||||
private void CreateDefaultSampleData()
|
||||
{
|
||||
if (m_DefaultDataCreated) return;
|
||||
m_DefaultDataCreated = true;
|
||||
|
||||
string[,] sample = new string[2, 3]
|
||||
{
|
||||
{ "Name", "Age", "City" },
|
||||
{ "Alice", "25", "NYC" }
|
||||
};
|
||||
|
||||
for (int r = 0; r < 2; r++)
|
||||
for (int c = 0; c < 3; c++)
|
||||
SetCellTextInternal(r, c, sample[r, c], skipDirty: true);
|
||||
}
|
||||
|
||||
private void SetCellTextInternal(int row, int col, string text, bool skipDirty)
|
||||
{
|
||||
var data = m_TableData.GetCell(row, col);
|
||||
if (data == null)
|
||||
{
|
||||
data = new XTableCellData();
|
||||
m_TableData.SetCell(row, col, data);
|
||||
}
|
||||
data.Text = text;
|
||||
if (!skipDirty) MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 编辑器模式支持
|
||||
|
||||
[System.NonSerialized] private bool m_EditorUpdateSubscribed;
|
||||
|
||||
private void SubscribeEditorUpdate()
|
||||
{
|
||||
if (m_EditorUpdateSubscribed) return;
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.update += EditorUpdate;
|
||||
m_EditorUpdateSubscribed = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
private void UnsubscribeEditorUpdate()
|
||||
{
|
||||
if (!m_EditorUpdateSubscribed) return;
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.update -= EditorUpdate;
|
||||
m_EditorUpdateSubscribed = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void EditorUpdate()
|
||||
{
|
||||
if (this == null || !isActiveAndEnabled)
|
||||
{
|
||||
UnsubscribeEditorUpdate();
|
||||
return;
|
||||
}
|
||||
if (m_DirtyFlag.IsDirty)
|
||||
RefreshView();
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开方法
|
||||
|
||||
public void SetCellData(int row, int col, XTableCellData data)
|
||||
{
|
||||
m_TableData.SetCell(row, col, data);
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void SetCellText(int row, int col, string text)
|
||||
{
|
||||
SetCellTextInternal(row, col, text, skipDirty: false);
|
||||
}
|
||||
|
||||
public XTableCellData GetCellData(int row, int col)
|
||||
=> m_TableData?.GetCell(row, col);
|
||||
|
||||
public void MergeCells(int startRow, int startCol, int rowSpan, int colSpan)
|
||||
{
|
||||
m_TableData.MergeCells(startRow, startCol, rowSpan, colSpan);
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void UnmergeCells(int startRow, int startCol)
|
||||
{
|
||||
m_TableData.UnmergeCells(startRow, startCol);
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void SelectCell(int row, int col)
|
||||
{
|
||||
m_SelectedRow = row;
|
||||
m_SelectedCol = col;
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
m_SelectedRow = -1;
|
||||
m_SelectedCol = -1;
|
||||
MarkDirty(TableDirtyType.DataChanged);
|
||||
}
|
||||
|
||||
public void RefreshView()
|
||||
{
|
||||
if (m_TableData == null || m_Assembler == null) return;
|
||||
if (m_RowHeights == null || m_ColWidths == null) return;
|
||||
if (m_RowHeights.Length == 0 || m_ColWidths.Length == 0) return;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
if (!Application.isPlaying)
|
||||
Canvas.ForceUpdateCanvases();
|
||||
#endif
|
||||
|
||||
RecalculateTotalSize();
|
||||
UpdateVisibleRect();
|
||||
|
||||
// 更新背景尺寸
|
||||
if (m_BackgroundGo != null)
|
||||
{
|
||||
var bgRt = m_BackgroundGo.GetComponent<RectTransform>();
|
||||
bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight);
|
||||
}
|
||||
|
||||
// 回收池
|
||||
m_Assembler.ReturnAll();
|
||||
|
||||
// 计算可见范围
|
||||
int sr, er, sc, ec;
|
||||
CalcVisibleRange(out sr, out er, out sc, out ec);
|
||||
|
||||
if (er <= sr || ec <= sc) return;
|
||||
|
||||
// 渲染每个可见单元格
|
||||
RenderCells(sr, er, sc, ec);
|
||||
|
||||
m_DirtyFlag.Clear();
|
||||
OnTableRefreshed?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 背景
|
||||
|
||||
private void CreateBackground()
|
||||
{
|
||||
if (m_BackgroundGo != null) return;
|
||||
|
||||
m_BackgroundGo = new GameObject("_TableBackground",
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
m_BackgroundGo.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
|
||||
m_BackgroundGo.transform.SetParent(rectTransform, false);
|
||||
|
||||
RectTransform bgRt = m_BackgroundGo.GetComponent<RectTransform>();
|
||||
bgRt.anchorMin = new Vector2(0, 1);
|
||||
bgRt.anchorMax = new Vector2(0, 1);
|
||||
bgRt.pivot = new Vector2(0, 1);
|
||||
bgRt.anchoredPosition = Vector2.zero;
|
||||
bgRt.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight);
|
||||
|
||||
var bgImg = m_BackgroundGo.GetComponent<Image>();
|
||||
bgImg.color = new Color(0.18f, 0.18f, 0.18f, 1f);
|
||||
bgImg.raycastTarget = false;
|
||||
}
|
||||
|
||||
/// <summary>完全重建表格——销毁所有动态生成的对象并重新初始化</summary>
|
||||
[ContextMenu("Xeric UI/完全重建表格")]
|
||||
public void FullRebuild()
|
||||
{
|
||||
m_Assembler?.DestroyAll();
|
||||
m_DefaultDataCreated = false;
|
||||
|
||||
if (m_BackgroundGo != null)
|
||||
{
|
||||
if (Application.isPlaying) Destroy(m_BackgroundGo);
|
||||
else DestroyImmediate(m_BackgroundGo);
|
||||
m_BackgroundGo = null;
|
||||
}
|
||||
|
||||
m_TableData = new XTableData();
|
||||
|
||||
m_Assembler = new CellAssembler(rectTransform);
|
||||
RecalculateTotalSize();
|
||||
CreateDefaultSampleData();
|
||||
CreateBackground();
|
||||
MarkDirty(TableDirtyType.All);
|
||||
RefreshView();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 单元格渲染
|
||||
|
||||
private void RenderCells(int sr, int er, int sc, int ec)
|
||||
{
|
||||
Color fgColor = GetStyleColor(m_DefaultStyleNamespace, "/cell/fg/color",
|
||||
new Color(0.05f, 0.05f, 0.05f));
|
||||
Color selectionColor = new Color(0.18f, 0.42f, 0.82f, 0.25f);
|
||||
int fontSize = GetStyleInt(m_DefaultStyleNamespace, "/cell/font/size", 14);
|
||||
|
||||
for (int r = sr; r < er && r < m_RowHeights.Length; r++)
|
||||
{
|
||||
for (int c = sc; c < ec && c < m_ColWidths.Length; c++)
|
||||
{
|
||||
float cellX = GetColLeftX(c);
|
||||
float cellY = -GetRowTopY(r);
|
||||
float cellW = m_ColWidths[c];
|
||||
float cellH = m_RowHeights[r];
|
||||
XTableCellData data = m_TableData.GetCell(r, c);
|
||||
|
||||
// 选中高亮背景
|
||||
if (r == m_SelectedRow && c == m_SelectedCol)
|
||||
{
|
||||
var selImg = m_Assembler.GetImage("cell_select");
|
||||
selImg.Rect.anchoredPosition = new Vector2(cellX, cellY);
|
||||
selImg.Rect.sizeDelta = new Vector2(cellW, cellH);
|
||||
selImg.Image.color = selectionColor;
|
||||
}
|
||||
|
||||
// 水平网格线
|
||||
var hLine = m_Assembler.GetImage("cell_hline");
|
||||
hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH);
|
||||
hLine.Rect.sizeDelta = new Vector2(cellW, 1f);
|
||||
hLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f);
|
||||
|
||||
// 垂直网格线
|
||||
var vLine = m_Assembler.GetImage("cell_vline");
|
||||
vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - 1, cellY);
|
||||
vLine.Rect.sizeDelta = new Vector2(1f, cellH);
|
||||
vLine.Image.color = new Color(0.35f, 0.35f, 0.35f, 0.5f);
|
||||
|
||||
if (data == null) continue;
|
||||
|
||||
// 文本:与 cell 同位置同尺寸,TMP 内部 Midline 对齐实现居中
|
||||
if (!string.IsNullOrEmpty(data.Text))
|
||||
{
|
||||
var txt = m_Assembler.GetText("cell_text");
|
||||
txt.Rect.anchoredPosition = new Vector2(cellX + 4, cellY);
|
||||
txt.Rect.sizeDelta = new Vector2(cellW - 8, cellH);
|
||||
txt.Text.text = data.Text;
|
||||
txt.Text.color = fgColor;
|
||||
txt.Text.fontSize = fontSize;
|
||||
txt.Text.alignment = TextAlignmentOptions.Midline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 坐标计算
|
||||
|
||||
private float GetRowTopY(int row)
|
||||
{
|
||||
float y = 0;
|
||||
for (int i = 0; i < row && i < m_RowHeights.Length; i++)
|
||||
y += m_RowHeights[i];
|
||||
return y;
|
||||
}
|
||||
|
||||
private float GetColLeftX(int col)
|
||||
{
|
||||
float x = 0;
|
||||
for (int i = 0; i < col && i < m_ColWidths.Length; i++)
|
||||
x += m_ColWidths[i];
|
||||
return x;
|
||||
}
|
||||
|
||||
private void RecalculateTotalSize()
|
||||
{
|
||||
m_TotalWidth = 0;
|
||||
if (m_ColWidths != null)
|
||||
foreach (float w in m_ColWidths) m_TotalWidth += w;
|
||||
|
||||
m_TotalHeight = 0;
|
||||
if (m_RowHeights != null)
|
||||
foreach (float h in m_RowHeights) m_TotalHeight += h;
|
||||
|
||||
if (m_ScrollRect != null && m_ScrollRect.content != null)
|
||||
m_ScrollRect.content.sizeDelta = new Vector2(m_TotalWidth, m_TotalHeight);
|
||||
}
|
||||
|
||||
private void UpdateVisibleRect()
|
||||
{
|
||||
m_VisibleRect = new Rect(0, 0, m_TotalWidth, m_TotalHeight);
|
||||
|
||||
if (m_ScrollRect != null && m_ScrollRect.viewport != null)
|
||||
{
|
||||
RectTransform vt = m_ScrollRect.viewport;
|
||||
RectTransform ct = m_ScrollRect.content;
|
||||
if (vt != null && ct != null)
|
||||
{
|
||||
float sx = Mathf.Max(0, -ct.anchoredPosition.x);
|
||||
float sy = Mathf.Max(0, ct.anchoredPosition.y);
|
||||
m_VisibleRect = new Rect(sx, sy, vt.rect.width, vt.rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CalcVisibleRange(out int sr, out int er, out int sc, out int ec)
|
||||
{
|
||||
sr = 0; er = 0; sc = 0; ec = 0;
|
||||
|
||||
float accY = 0;
|
||||
for (int r = 0; r < m_RowHeights.Length; r++)
|
||||
{
|
||||
float rowBottom = accY + m_RowHeights[r];
|
||||
if (rowBottom > m_VisibleRect.y && accY < m_VisibleRect.yMax)
|
||||
{
|
||||
if (er == 0 && sr == 0) sr = r;
|
||||
er = r + 1;
|
||||
}
|
||||
accY = rowBottom;
|
||||
if (accY > m_VisibleRect.yMax) break;
|
||||
}
|
||||
|
||||
float accX = 0;
|
||||
for (int c = 0; c < m_ColWidths.Length; c++)
|
||||
{
|
||||
float colRight = accX + m_ColWidths[c];
|
||||
if (colRight > m_VisibleRect.x && accX < m_VisibleRect.xMax)
|
||||
{
|
||||
if (ec == 0 && sc == 0) sc = c;
|
||||
ec = c + 1;
|
||||
}
|
||||
accX = colRight;
|
||||
if (accX > m_VisibleRect.xMax) break;
|
||||
}
|
||||
|
||||
if (sr < 0) sr = 0;
|
||||
if (sc < 0) sc = 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 样式
|
||||
|
||||
private Color GetStyleColor(string ns, string path, Color fallback)
|
||||
{
|
||||
StyleValue val = StyleManager.GetValue(ns, path);
|
||||
return val != null ? (Color)val : fallback;
|
||||
}
|
||||
|
||||
private int GetStyleInt(string ns, string path, int fallback)
|
||||
{
|
||||
StyleValue val = StyleManager.GetValue(ns, path);
|
||||
return val != null ? (int)val : fallback;
|
||||
}
|
||||
|
||||
private void EnsureDefaultStyleNamespace()
|
||||
{
|
||||
if (!StyleManager.HasStyle(m_DefaultStyleNamespace, "/cell/bg/color"))
|
||||
{
|
||||
StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/bg/color",
|
||||
new Color(0.18f, 0.18f, 0.18f));
|
||||
StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/fg/color",
|
||||
new Color(0.05f, 0.05f, 0.05f));
|
||||
StyleManager.AddDynamicStyle(m_DefaultStyleNamespace, "/cell/font/size", 14);
|
||||
StyleManager.ForceRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 其他
|
||||
|
||||
private void MarkDirty(TableDirtyType type) => m_DirtyFlag.Mark(type);
|
||||
|
||||
private void OnScrollValueChanged(Vector2 _) => MarkDirty(TableDirtyType.ViewChanged);
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPointerClickHandler
|
||||
|
||||
public void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
rectTransform, eventData.position, eventData.pressEventCamera, out Vector2 localPoint);
|
||||
|
||||
// rectTransform 锚点在左上角,localPoint 以中心为原点
|
||||
float tableX = localPoint.x - rectTransform.rect.xMin;
|
||||
float tableY = rectTransform.rect.yMax - localPoint.y;
|
||||
|
||||
float scrollY = m_ScrollRect != null ? Mathf.Max(0, m_ScrollRect.content.anchoredPosition.y) : 0;
|
||||
float scrollX = m_ScrollRect != null ? Mathf.Max(0, -m_ScrollRect.content.anchoredPosition.x) : 0;
|
||||
tableX += scrollX;
|
||||
tableY += scrollY;
|
||||
|
||||
int col = -1;
|
||||
float ax = 0;
|
||||
for (int c = 0; c < m_ColWidths.Length; c++)
|
||||
{
|
||||
if (tableX >= ax && tableX < ax + m_ColWidths[c]) { col = c; break; }
|
||||
ax += m_ColWidths[c];
|
||||
}
|
||||
|
||||
int row = -1;
|
||||
float ay = 0;
|
||||
for (int r = 0; r < m_RowHeights.Length; r++)
|
||||
{
|
||||
if (tableY >= ay && tableY < ay + m_RowHeights[r]) { row = r; break; }
|
||||
ay += m_RowHeights[r];
|
||||
}
|
||||
|
||||
if (row >= 0 && col >= 0)
|
||||
{
|
||||
SelectCell(row, col);
|
||||
OnCellSelected?.Invoke(row, col);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13944afb118d5de499192b64114ec4f2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 238b49eb1d61170498956f869ee62303
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,268 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using TMPro;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Elements
|
||||
{
|
||||
/// <summary>
|
||||
/// 表格单元动态拼装器——通过对象池管理 Image + TMP_Text 的组合创建与回收。
|
||||
/// 所有元素统一使用 anchor=(0,1) pivot=(0,1) 左上角坐标系。
|
||||
/// 所有动态生成对象标记 HideFlags.DontSave | HideFlags.HideInHierarchy,防止泄漏到场景。
|
||||
/// </summary>
|
||||
public class CellAssembler
|
||||
{
|
||||
#region 对象池定义
|
||||
|
||||
public class PooledImage
|
||||
{
|
||||
public GameObject Root;
|
||||
public RectTransform Rect;
|
||||
public Image Image;
|
||||
public StyleBoundElement Style;
|
||||
}
|
||||
|
||||
public class PooledText
|
||||
{
|
||||
public GameObject Root;
|
||||
public RectTransform Rect;
|
||||
public TMP_Text Text;
|
||||
public StyleBoundElement Style;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字段
|
||||
|
||||
private Transform m_Parent;
|
||||
private Transform m_PoolRoot;
|
||||
|
||||
private Stack<PooledImage> m_ImagePool = new Stack<PooledImage>();
|
||||
private Stack<PooledText> m_TextPool = new Stack<PooledText>();
|
||||
private List<PooledImage> m_ActiveImages = new List<PooledImage>();
|
||||
private List<PooledText> m_ActiveTexts = new List<PooledText>();
|
||||
|
||||
/// <summary>HideFlags 应用于所有动态创建的对象(不保存、不在 Hierarchy 显示)</summary>
|
||||
private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数与析构
|
||||
|
||||
public CellAssembler(Transform parent)
|
||||
{
|
||||
m_Parent = parent;
|
||||
|
||||
GameObject poolRoot = new GameObject("_CellPool");
|
||||
poolRoot.hideFlags = POOL_FLAGS;
|
||||
poolRoot.transform.SetParent(parent, false);
|
||||
poolRoot.SetActive(false);
|
||||
m_PoolRoot = poolRoot.transform;
|
||||
}
|
||||
|
||||
/// <summary>彻底销毁所有池对象(包括活跃和休眠的)</summary>
|
||||
public void DestroyAll()
|
||||
{
|
||||
// 杀死活跃对象
|
||||
for (int i = m_ActiveImages.Count - 1; i >= 0; i--)
|
||||
{
|
||||
KillObject(m_ActiveImages[i].Root);
|
||||
}
|
||||
m_ActiveImages.Clear();
|
||||
|
||||
for (int i = m_ActiveTexts.Count - 1; i >= 0; i--)
|
||||
{
|
||||
KillObject(m_ActiveTexts[i].Root);
|
||||
}
|
||||
m_ActiveTexts.Clear();
|
||||
|
||||
// 杀死池中对象
|
||||
while (m_ImagePool.Count > 0)
|
||||
{
|
||||
KillObject(m_ImagePool.Pop().Root);
|
||||
}
|
||||
while (m_TextPool.Count > 0)
|
||||
{
|
||||
KillObject(m_TextPool.Pop().Root);
|
||||
}
|
||||
|
||||
// 杀死池根
|
||||
if (m_PoolRoot != null)
|
||||
{
|
||||
KillObject(m_PoolRoot.gameObject);
|
||||
m_PoolRoot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void KillObject(GameObject go)
|
||||
{
|
||||
if (go == null) return;
|
||||
if (Application.isPlaying)
|
||||
Object.Destroy(go);
|
||||
else
|
||||
Object.DestroyImmediate(go);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 常量
|
||||
|
||||
private static readonly Vector2 ANCHOR_TOPLEFT_MIN = new Vector2(0, 1);
|
||||
private static readonly Vector2 ANCHOR_TOPLEFT_MAX = new Vector2(0, 1);
|
||||
private static readonly Vector2 PIVOT_TOPLEFT = new Vector2(0, 1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 池操作
|
||||
|
||||
public PooledImage GetImage(string name = "cell_img")
|
||||
{
|
||||
PooledImage item;
|
||||
if (m_ImagePool.Count > 0)
|
||||
{
|
||||
item = m_ImagePool.Pop();
|
||||
item.Root.SetActive(true);
|
||||
item.Root.transform.SetParent(m_Parent, false);
|
||||
ResetImageDefaults(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
item = CreateImage(name);
|
||||
}
|
||||
m_ActiveImages.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public PooledText GetText(string name = "cell_text")
|
||||
{
|
||||
PooledText item;
|
||||
if (m_TextPool.Count > 0)
|
||||
{
|
||||
item = m_TextPool.Pop();
|
||||
item.Root.SetActive(true);
|
||||
item.Root.transform.SetParent(m_Parent, false);
|
||||
ResetTextDefaults(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
item = CreateText(name);
|
||||
}
|
||||
m_ActiveTexts.Add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
public void ReturnAll()
|
||||
{
|
||||
foreach (var img in m_ActiveImages) ReturnImage(img);
|
||||
m_ActiveImages.Clear();
|
||||
|
||||
foreach (var txt in m_ActiveTexts) ReturnText(txt);
|
||||
m_ActiveTexts.Clear();
|
||||
}
|
||||
|
||||
private void ReturnImage(PooledImage item)
|
||||
{
|
||||
item.Style?.Unbind();
|
||||
item.Root.SetActive(false);
|
||||
item.Root.transform.SetParent(m_PoolRoot, false);
|
||||
m_ImagePool.Push(item);
|
||||
}
|
||||
|
||||
private void ReturnText(PooledText item)
|
||||
{
|
||||
item.Style?.Unbind();
|
||||
item.Text.text = "";
|
||||
item.Root.SetActive(false);
|
||||
item.Root.transform.SetParent(m_PoolRoot, false);
|
||||
m_TextPool.Push(item);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 默认值重置
|
||||
|
||||
private void ResetImageDefaults(PooledImage item)
|
||||
{
|
||||
item.Rect.anchorMin = ANCHOR_TOPLEFT_MIN;
|
||||
item.Rect.anchorMax = ANCHOR_TOPLEFT_MAX;
|
||||
item.Rect.pivot = PIVOT_TOPLEFT;
|
||||
item.Rect.anchoredPosition = Vector2.zero;
|
||||
item.Rect.sizeDelta = Vector2.zero;
|
||||
item.Image.color = Color.white;
|
||||
item.Image.sprite = null;
|
||||
item.Image.raycastTarget = false;
|
||||
}
|
||||
|
||||
private void ResetTextDefaults(PooledText item)
|
||||
{
|
||||
item.Rect.anchorMin = ANCHOR_TOPLEFT_MIN;
|
||||
item.Rect.anchorMax = ANCHOR_TOPLEFT_MAX;
|
||||
item.Rect.pivot = PIVOT_TOPLEFT;
|
||||
item.Rect.anchoredPosition = Vector2.zero;
|
||||
item.Rect.sizeDelta = Vector2.zero;
|
||||
item.Text.text = "";
|
||||
item.Text.color = Color.black;
|
||||
item.Text.fontSize = 14;
|
||||
item.Text.alignment = TextAlignmentOptions.Midline;
|
||||
item.Text.raycastTarget = false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 创建
|
||||
|
||||
private PooledImage CreateImage(string name)
|
||||
{
|
||||
GameObject go = new GameObject(name,
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
|
||||
go.hideFlags = POOL_FLAGS;
|
||||
go.transform.SetParent(m_Parent, false);
|
||||
|
||||
Image img = go.GetComponent<Image>();
|
||||
img.raycastTarget = false;
|
||||
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.anchorMin = ANCHOR_TOPLEFT_MIN;
|
||||
rect.anchorMax = ANCHOR_TOPLEFT_MAX;
|
||||
rect.pivot = PIVOT_TOPLEFT;
|
||||
|
||||
return new PooledImage
|
||||
{
|
||||
Root = go,
|
||||
Rect = rect,
|
||||
Image = img,
|
||||
Style = new StyleBoundElement { ImageComponent = img }
|
||||
};
|
||||
}
|
||||
|
||||
private PooledText CreateText(string name)
|
||||
{
|
||||
GameObject go = new GameObject(name,
|
||||
typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
|
||||
go.hideFlags = POOL_FLAGS;
|
||||
go.transform.SetParent(m_Parent, false);
|
||||
|
||||
TMP_Text text = go.GetComponent<TMP_Text>();
|
||||
text.raycastTarget = false;
|
||||
text.alignment = TextAlignmentOptions.Midline;
|
||||
text.fontSize = 14;
|
||||
|
||||
RectTransform rect = go.GetComponent<RectTransform>();
|
||||
rect.anchorMin = ANCHOR_TOPLEFT_MIN;
|
||||
rect.anchorMax = ANCHOR_TOPLEFT_MAX;
|
||||
rect.pivot = PIVOT_TOPLEFT;
|
||||
|
||||
return new PooledText
|
||||
{
|
||||
Root = go,
|
||||
Rect = rect,
|
||||
Text = text,
|
||||
Style = new StyleBoundElement { TextComponent = text }
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9dd186d27001684f897c8ca38815c74
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,170 @@
|
||||
using TMPro;
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
using XericLibrary.Runtime.SuperStyleSheet;
|
||||
|
||||
namespace XericUI.XTable.Rendering.Elements
|
||||
{
|
||||
/// <summary>
|
||||
/// 样式绑定元素——将 Image 或 TMP_Text 挂勾到样式表路径。
|
||||
/// 自动从 StyleManager 拉取值并应用,样式变更时自动刷新。
|
||||
/// 通过对象池复用时调用 Bind/Unbind 切换绑定目标。
|
||||
/// </summary>
|
||||
public class StyleBoundElement
|
||||
{
|
||||
#region 字段
|
||||
|
||||
/// <summary>绑定的 Image 组件(可能为 null)</summary>
|
||||
public Image ImageComponent;
|
||||
|
||||
/// <summary>绑定的 TMP_Text 组件(可能为 null)</summary>
|
||||
public TMP_Text TextComponent;
|
||||
|
||||
/// <summary>样式命名空间</summary>
|
||||
private string m_StyleNamespace;
|
||||
|
||||
/// <summary>各路径的 StyleField 缓存</summary>
|
||||
private System.Collections.Generic.Dictionary<string, StyleField> m_Fields
|
||||
= new System.Collections.Generic.Dictionary<string, StyleField>();
|
||||
|
||||
/// <summary>是否已绑定</summary>
|
||||
private bool m_IsBound;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公开方法
|
||||
|
||||
/// <summary>
|
||||
/// 绑定到指定命名空间并开始监听样式。
|
||||
/// </summary>
|
||||
public void Bind(string styleNamespace)
|
||||
{
|
||||
if (m_IsBound) Unbind();
|
||||
m_StyleNamespace = styleNamespace;
|
||||
|
||||
if (string.IsNullOrEmpty(styleNamespace)) return;
|
||||
|
||||
// 预注册常用路径
|
||||
RegisterField("/cell/bg/color", OnStyleChanged);
|
||||
RegisterField("/cell/fg/color", OnStyleChanged);
|
||||
RegisterField("/cell/border/top/color", OnStyleChanged);
|
||||
RegisterField("/cell/border/bottom/color", OnStyleChanged);
|
||||
RegisterField("/cell/border/left/color", OnStyleChanged);
|
||||
RegisterField("/cell/border/right/color", OnStyleChanged);
|
||||
RegisterField("/cell/font/size", OnStyleChanged);
|
||||
RegisterField("/cell/font/alignment", OnStyleChanged);
|
||||
RegisterField("/cell/font/richText", OnStyleChanged);
|
||||
|
||||
m_IsBound = true;
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解绑并停止监听样式。
|
||||
/// </summary>
|
||||
public void Unbind()
|
||||
{
|
||||
if (!m_IsBound) return;
|
||||
|
||||
foreach (var kvp in m_Fields)
|
||||
{
|
||||
if (kvp.Value != null)
|
||||
{
|
||||
kvp.Value.OnValueChanged -= OnStyleChanged;
|
||||
kvp.Value.Unregister();
|
||||
}
|
||||
}
|
||||
m_Fields.Clear();
|
||||
m_IsBound = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新全部样式值到组件。
|
||||
/// </summary>
|
||||
public void RefreshAll()
|
||||
{
|
||||
if (!m_IsBound || string.IsNullOrEmpty(m_StyleNamespace)) return;
|
||||
|
||||
// 背景颜色 → Image.color
|
||||
StyleValue bgVal = GetStyleValue("/cell/bg/color");
|
||||
if (bgVal != null && ImageComponent != null)
|
||||
ImageComponent.color = bgVal;
|
||||
|
||||
// 前景颜色 → Text.color
|
||||
StyleValue fgVal = GetStyleValue("/cell/fg/color");
|
||||
if (fgVal != null && TextComponent != null)
|
||||
TextComponent.color = fgVal;
|
||||
|
||||
// 字体大小
|
||||
StyleValue sizeVal = GetStyleValue("/cell/font/size");
|
||||
if (sizeVal != null && TextComponent != null)
|
||||
TextComponent.fontSize = (int)sizeVal;
|
||||
|
||||
// 对齐
|
||||
StyleValue alignVal = GetStyleValue("/cell/font/alignment");
|
||||
if (alignVal != null && TextComponent != null)
|
||||
TextComponent.alignment = alignVal.Convert<TextAlignmentOptions>();
|
||||
|
||||
// 富文本
|
||||
StyleValue richVal = GetStyleValue("/cell/font/richText");
|
||||
if (richVal != null && TextComponent != null)
|
||||
TextComponent.richText = (bool)richVal;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有方法
|
||||
|
||||
private void RegisterField(string path, System.Action<StyleValue> callback)
|
||||
{
|
||||
var field = new StyleField
|
||||
{
|
||||
Namespace = m_StyleNamespace,
|
||||
StylePath = path
|
||||
};
|
||||
field.Register();
|
||||
field.OnValueChanged += callback;
|
||||
m_Fields[path] = field;
|
||||
}
|
||||
|
||||
private StyleValue GetStyleValue(string path)
|
||||
{
|
||||
return StyleManager.GetValue(m_StyleNamespace, path);
|
||||
}
|
||||
|
||||
private void OnStyleChanged(StyleValue val)
|
||||
{
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用于 TextAnchor → TextAlignmentOptions 的转换扩展。
|
||||
/// </summary>
|
||||
internal static class StyleBoundElementExtensions
|
||||
{
|
||||
public static TextAlignmentOptions Convert<T>(this StyleValue val)
|
||||
{
|
||||
string s = val?.GetValueAs<string>();
|
||||
if (string.IsNullOrEmpty(s)) return TextAlignmentOptions.Midline;
|
||||
|
||||
switch (s)
|
||||
{
|
||||
case "UpperLeft": return TextAlignmentOptions.TopLeft;
|
||||
case "UpperCenter": return TextAlignmentOptions.Top;
|
||||
case "UpperRight": return TextAlignmentOptions.TopRight;
|
||||
case "MiddleLeft": return TextAlignmentOptions.MidlineLeft;
|
||||
case "MiddleCenter": return TextAlignmentOptions.Midline;
|
||||
case "MiddleRight": return TextAlignmentOptions.MidlineRight;
|
||||
case "LowerLeft": return TextAlignmentOptions.BottomLeft;
|
||||
case "LowerCenter": return TextAlignmentOptions.Bottom;
|
||||
case "LowerRight": return TextAlignmentOptions.BottomRight;
|
||||
default: return TextAlignmentOptions.Midline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd19dc2a8b3f43348a76e2d1cb8e228e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7367518f7495815439e1a32ee820f1c5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.XTable.Rendering.UIToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// UI Toolkit 渲染器(预留)——后续实现表格在 UI Toolkit 上的渲染。
|
||||
/// 使用 Unity 原生 CSS 样式表。
|
||||
/// </summary>
|
||||
public class XTableUITKRenderer
|
||||
{
|
||||
// TODO: 实现 UI Toolkit 渲染逻辑
|
||||
// - 使用 VisualElement 构建表格
|
||||
// - 通过 USS (Unity Style Sheet) 控制样式
|
||||
// - 实现虚拟化滚动
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b2e42673c88a2e40853175ade86ec7b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,107 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.XTable.Rendering
|
||||
{
|
||||
/// <summary>
|
||||
/// 左上角坐标系的视图矩形。
|
||||
/// Unity 的 Rect 是左下角原点(Y-up),表格数据索引是左上角原点(Y-down)。
|
||||
/// 此类负责坐标转换和可见单元格范围计算。
|
||||
/// </summary>
|
||||
public struct XTableViewRect
|
||||
{
|
||||
/// <summary>左上角 X 坐标</summary>
|
||||
public float X;
|
||||
|
||||
/// <summary>左上角 Y 坐标(从上到下递增)</summary>
|
||||
public float Y;
|
||||
|
||||
/// <summary>视图宽度</summary>
|
||||
public float Width;
|
||||
|
||||
/// <summary>视图高度</summary>
|
||||
public float Height;
|
||||
|
||||
/// <summary>右边界</summary>
|
||||
public float XMax => X + Width;
|
||||
|
||||
/// <summary>下边界</summary>
|
||||
public float YMax => Y + Height;
|
||||
|
||||
/// <summary>
|
||||
/// 从 Unity Rect(左下角原点)转换为 TableViewRect(左上角原点)
|
||||
/// </summary>
|
||||
/// <param name="unityRect">Unity 坐标系下的矩形</param>
|
||||
/// <param name="containerHeight">容器总高度(用于 Y 轴翻转)</param>
|
||||
public static XTableViewRect FromUnityRect(Rect unityRect, float containerHeight)
|
||||
{
|
||||
return new XTableViewRect
|
||||
{
|
||||
X = unityRect.xMin,
|
||||
Y = containerHeight - unityRect.yMax,
|
||||
Width = unityRect.width,
|
||||
Height = unityRect.height
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为 Unity Rect(左下角原点)
|
||||
/// </summary>
|
||||
public Rect ToUnityRect(float containerHeight)
|
||||
{
|
||||
return new Rect(X, containerHeight - Y - Height, Width, Height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过行列尺寸计算可见单元格范围。
|
||||
/// 累加行高/列宽直到超出视图范围。
|
||||
/// </summary>
|
||||
/// <param name="rowHeights">每行高度数组</param>
|
||||
/// <param name="colWidths">每列宽度数组</param>
|
||||
/// <param name="startRow">可见起始行(包含)</param>
|
||||
/// <param name="endRow">可见结束行(不包含)</param>
|
||||
/// <param name="startCol">可见起始列(包含)</param>
|
||||
/// <param name="endCol">可见结束列(不包含)</param>
|
||||
public void GetVisibleCellRange(
|
||||
float[] rowHeights, float[] colWidths,
|
||||
out int startRow, out int endRow,
|
||||
out int startCol, out int endCol)
|
||||
{
|
||||
startRow = 0;
|
||||
endRow = 0;
|
||||
startCol = 0;
|
||||
endCol = 0;
|
||||
|
||||
if (rowHeights == null || colWidths == null) return;
|
||||
|
||||
// 计算行范围(Y 方向,从上到下)
|
||||
float accumulatedY = 0f;
|
||||
for (int r = 0; r < rowHeights.Length; r++)
|
||||
{
|
||||
float rowBottom = accumulatedY + rowHeights[r];
|
||||
if (rowBottom > Y && accumulatedY < YMax)
|
||||
{
|
||||
if (startRow == 0 && accumulatedY < YMax)
|
||||
startRow = r;
|
||||
endRow = r + 1;
|
||||
}
|
||||
accumulatedY = rowBottom;
|
||||
if (accumulatedY > YMax) break;
|
||||
}
|
||||
|
||||
// 计算列范围(X 方向,从左到右)
|
||||
float accumulatedX = 0f;
|
||||
for (int c = 0; c < colWidths.Length; c++)
|
||||
{
|
||||
float colRight = accumulatedX + colWidths[c];
|
||||
if (colRight > X && accumulatedX < XMax)
|
||||
{
|
||||
if (startCol == 0 && accumulatedX < XMax)
|
||||
startCol = c;
|
||||
endCol = c + 1;
|
||||
}
|
||||
accumulatedX = colRight;
|
||||
if (accumulatedX > XMax) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f64af1d0c0781de499619595c05d0061
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 64d88208839a5034db31c330796453aa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
// ==========================================
|
||||
// Xeric UI Action Table - Default Style Sheet
|
||||
// 表格默认样式定义
|
||||
// ==========================================
|
||||
|
||||
namespace: xeric_table_default
|
||||
|
||||
// ---- 单元格背景 ----
|
||||
/cell/bg/color = (1,1,1,1):UnityEngine.Color @Desc:单元格背景颜色
|
||||
|
||||
// ---- 单元格前景 ----
|
||||
/cell/fg/color = (0.1,0.1,0.1,1):UnityEngine.Color @Desc:单元格前景文字颜色
|
||||
|
||||
// ---- 边框 ----
|
||||
/cell/border/top/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:上边框颜色
|
||||
/cell/border/top/width = 1:System.Single @Desc:上边框宽度
|
||||
/cell/border/bottom/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:下边框颜色
|
||||
/cell/border/bottom/width = 1:System.Single @Desc:下边框宽度
|
||||
/cell/border/left/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:左边框颜色
|
||||
/cell/border/left/width = 1:System.Single @Desc:左边框宽度
|
||||
/cell/border/right/color = (0.8,0.8,0.8,1):UnityEngine.Color @Desc:右边框颜色
|
||||
/cell/border/right/width = 1:System.Single @Desc:右边框宽度
|
||||
|
||||
// ---- 选中高亮 ----
|
||||
/cell/selection/bg/color = (0.2,0.5,1,0.3):UnityEngine.Color @Desc:选中单元格高亮颜色
|
||||
|
||||
// ---- 表头样式 ----
|
||||
/header/bg/color = (0.9,0.9,0.9,1):UnityEngine.Color @Desc:表头背景颜色
|
||||
/header/fg/color = (0,0,0,1):UnityEngine.Color @Desc:表头文字颜色
|
||||
|
||||
// ---- 字体 ----
|
||||
/cell/font/size = 14:System.Int32 @Desc:默认字体大小
|
||||
/cell/font/alignment = MiddleCenter:UnityEngine.TextAnchor @Desc:默认文本对齐
|
||||
/cell/font/richText = true:System.Boolean @Desc:是否启用富文本
|
||||
|
||||
// ---- 行高 / 列宽(缺省值) ----
|
||||
/cell/default/height = 40:System.Single @Desc:默认行高
|
||||
/cell/default/width = 100:System.Single @Desc:默认列宽
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2465cbc9f8bf384aa95561b40bd4841
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 1693830282, guid: 65aae7f37e34be6409e0966d54c89b6d, type: 3}
|
||||
Reference in New Issue
Block a user