1.重构UXButton
2.主Transition有UXSeletable负责 ,同时兼具导航
3.新增UXSlider  支持平滑过渡 适配手柄
4.优化部分逻辑bug
This commit is contained in:
陈思海 2025-12-09 15:08:41 +08:00
parent 132426b9b8
commit 32c0fc13fd
44 changed files with 2807 additions and 987 deletions

View File

@ -3,7 +3,7 @@ using System.Reflection;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI.Extension.Editor namespace AlicizaX.UI.Extension
{ {
internal static class ExtensionHelper internal static class ExtensionHelper
{ {

View File

@ -2,7 +2,7 @@ using System;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI.Extension.Editor namespace AlicizaX.UI.Extension
{ {
internal class GUILayoutHelper internal class GUILayoutHelper
{ {

View File

@ -40,6 +40,22 @@ public class UXCreateHelper : Editor
image.material = AssetDatabase.LoadAssetAtPath<Material>(UXGUIConfig.UIDefaultMatPath); image.material = AssetDatabase.LoadAssetAtPath<Material>(UXGUIConfig.UIDefaultMatPath);
} }
[MenuItem("GameObject/UI/UXSlider")]
public static void CreateUXSlider(MenuCommand menuCommand)
{
Type MenuOptionsType = typeof(UnityEditor.UI.SliderEditor).Assembly.GetType("UnityEditor.UI.MenuOptions");
InvokeMethod(MenuOptionsType, "AddSlider", new object[] { menuCommand });
GameObject obj = Selection.activeGameObject;
obj.name = "UXSlider";
DestroyImmediate(obj.GetComponent<Slider>());
var uxSlider = obj.AddComponent<UXSlider>();
uxSlider.fillRect = obj.transform.Find("Fill Area/Fill").GetComponent<RectTransform>();
var handle = obj.transform.Find("Handle Slide Area/Handle").GetComponent<RectTransform>();
uxSlider.handleRect = handle;
uxSlider.targetGraphic = handle.GetComponent<Graphic>();
}
#if TEXTMESHPRO_SUPPORT #if TEXTMESHPRO_SUPPORT
[MenuItem("GameObject/UI/UXTextMeshPro")] [MenuItem("GameObject/UI/UXTextMeshPro")]

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

View File

@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 1ab39039c9d7d844aa962517519f0ad6
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 0
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

View File

@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: dfe8ade815753dc4d9ca3ce5d981cb91
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 0
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: WebGL
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

3
Editor/UX/Button.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: baf1c03f83bf4c46aaf15b1cb5ff48cd
timeCreated: 1765184499

View File

@ -0,0 +1,402 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AlicizaX.UI.Extension;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEditor.UI;
using UnityEngine;
using UnityEngine.UI;
using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType;
[CanEditMultipleObjects]
[CustomEditor(typeof(UXButton), true)]
internal class UXButtonEditor : UXSelectableEditor
{
private enum TabType
{
Image,
Sound,
Event
}
private SerializedProperty m_Mode;
private SerializedProperty m_OnValueChanged;
private SerializedProperty m_OnClick;
private SerializedProperty m_UXGroup;
private SerializedProperty m_ChildTransitions;
private ReorderableList m_ChildTransitionList;
private static Color darkZebraEven = new Color(0.22f, 0.22f, 0.22f);
private static Color darkZebraOdd = new Color(0.27f, 0.27f, 0.27f);
private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip;
private UXButton mTarget;
protected override void OnEnable()
{
base.OnEnable();
mTarget = (UXButton)target;
m_UXGroup = serializedObject.FindProperty("m_UXGroup");
m_Mode = serializedObject.FindProperty("m_Mode");
m_OnValueChanged = serializedObject.FindProperty("m_OnValueChanged");
m_OnClick = serializedObject.FindProperty("m_OnClick");
m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions");
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
clickAudioClip = serializedObject.FindProperty("clickAudioClip");
CreateChildTransitionList();
// 不再 Register Image基类已经创建改为 Append 到 base 的 Image 页签
AppendToTab("Image", DrawImageHead, false);
AppendToTab("Image", DrawImageBottom);
// 另外注册独立的 Sound / Event 页签
RegisterTab("Sound", "d_AudioSource Icon", DrawSoundTab);
RegisterTab("Event", "EventTrigger Icon", DrawEventTab);
}
protected override void OnDisable()
{
// 移除之前追加到 Image 页签的回调,避免残留
RemoveCallbackFromTab("Image", DrawImageHead);
UnregisterTab("Sound");
UnregisterTab("Event");
base.OnDisable();
}
private void CreateChildTransitionList()
{
m_ChildTransitionList = new ReorderableList(serializedObject, m_ChildTransitions, true, false, true, true);
m_ChildTransitionList.drawElementBackgroundCallback = (rect, index, isActive, isFocused) =>
{
var background = index % 2 == 0 ? darkZebraEven : darkZebraOdd;
EditorGUI.DrawRect(rect, background);
};
m_ChildTransitionList.drawElementCallback = (rect, index, isActive, isFocused) =>
{
var element = m_ChildTransitionList.serializedProperty.GetArrayElementAtIndex(index);
rect.y += 2;
string elementTitle = $"Null Transition";
var targetProp = element.FindPropertyRelative("targetGraphic");
if (targetProp.objectReferenceValue != null)
elementTitle = targetProp.objectReferenceValue.name;
EditorGUI.LabelField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight),
elementTitle, EditorStyles.boldLabel);
rect.y += EditorGUIUtility.singleLineHeight + 2;
DrawTransitionData(new Rect(rect.x, rect.y, rect.width, 0), element, true);
};
m_ChildTransitionList.elementHeightCallback = (index) =>
{
return EditorGUIUtility.singleLineHeight +
CalculateTransitionDataHeight(m_ChildTransitionList.serializedProperty.GetArrayElementAtIndex(index)) +
10;
};
m_ChildTransitionList.onAddCallback = (list) =>
{
list.serializedProperty.arraySize++;
serializedObject.ApplyModifiedProperties();
};
m_ChildTransitionList.onRemoveCallback = (list) => { ReorderableList.defaultBehaviours.DoRemoveButton(list); };
}
// 追加到 base Image 页签的内容
private void DrawImageHead()
{
EditorGUI.BeginDisabledGroup(EditorApplication.isPlaying);
var modeType = (ButtonModeType)EditorGUILayout.EnumPopup("Mode", (ButtonModeType)m_Mode.enumValueIndex);
if (modeType != (ButtonModeType)m_Mode.enumValueIndex)
{
if (modeType == ButtonModeType.Normal)
{
ResetEventProperty(m_OnValueChanged);
m_UXGroup.objectReferenceValue = null;
}
else
{
ResetEventProperty(m_OnClick);
}
m_Mode.enumValueIndex = (int)modeType;
}
EditorGUI.EndDisabledGroup();
}
private void DrawImageBottom()
{
GUILayout.Space(5);
DrawChildTransitions();
GUILayout.Space(1);
DrawBasicSettings();
}
private void DrawSoundTab()
{
GUILayoutHelper.DrawProperty(hoverAudioClip, customSkin, "Hover Sound", "Play", () =>
{
if (hoverAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue);
}
});
GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () =>
{
if (clickAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)clickAudioClip.objectReferenceValue);
}
});
}
protected virtual void DrawEventTab()
{
if (m_Mode.enumValueIndex == (int)ButtonModeType.Toggle)
{
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_OnValueChanged);
}
else
{
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_OnClick);
}
EditorGUILayout.Separator();
}
// helpers kept from original file
private void ResetEventProperty(SerializedProperty property)
{
SerializedProperty persistentCalls = property.FindPropertyRelative("m_PersistentCalls");
SerializedProperty calls = persistentCalls.FindPropertyRelative("m_Calls");
calls.arraySize = 0;
property.serializedObject.ApplyModifiedProperties();
}
private void PlayAudio(AudioClip clip)
{
if (clip != null)
{
ExtensionHelper.PreviewAudioClip(clip);
}
}
private void DrawBasicSettings()
{
if (m_Mode.enumValueIndex == (int)ButtonModeType.Toggle)
{
GUILayoutHelper.DrawProperty<UXGroup>(m_UXGroup, customSkin, "UXGroup", (oldValue, newValue) =>
{
UXButton self = target as UXButton;
if (oldValue != null)
{
oldValue.UnregisterButton(self);
}
if (newValue != null)
{
newValue.RegisterButton(self);
}
});
}
}
private void DrawChildTransitions()
{
m_ChildTransitionList.DoLayoutList();
}
// CalculateTransitionDataHeight & DrawTransitionData remain the same as earlier to preserve child-only Animation restriction and warnings.
private float CalculateTransitionDataHeight(SerializedProperty transitionData)
{
float height = 0;
SerializedProperty transition = transitionData.FindPropertyRelative("transition");
var currentTransition = GetTransition(transition);
bool isChild = m_ChildTransitions != null &&
!string.IsNullOrEmpty(m_ChildTransitions.propertyPath) &&
transitionData.propertyPath.StartsWith(m_ChildTransitions.propertyPath);
if (isChild && currentTransition == Selectable.Transition.Animation)
{
currentTransition = Selectable.Transition.ColorTint;
}
height += EditorGUIUtility.singleLineHeight * 1.5f;
height += EditorGUIUtility.singleLineHeight;
SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic");
var graphic = targetGraphic.objectReferenceValue as Graphic;
var animator = graphic != null ? graphic.GetComponent<Animator>() : null;
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
if (graphic == null) height += EditorGUIUtility.singleLineHeight;
break;
case Selectable.Transition.SpriteSwap:
if (!(graphic is Image)) height += EditorGUIUtility.singleLineHeight;
break;
case Selectable.Transition.Animation:
if (animator == null) height += EditorGUIUtility.singleLineHeight;
break;
}
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("colors"));
break;
case Selectable.Transition.SpriteSwap:
height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("spriteState"));
break;
case Selectable.Transition.Animation:
height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("animationTriggers"));
break;
}
return height;
}
private void DrawTransitionData(Rect position, SerializedProperty transitionData, bool isChild = false)
{
SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic");
SerializedProperty transition = transitionData.FindPropertyRelative("transition");
SerializedProperty colorBlock = transitionData.FindPropertyRelative("colors");
SerializedProperty spriteState = transitionData.FindPropertyRelative("spriteState");
SerializedProperty animationTriggers = transitionData.FindPropertyRelative("animationTriggers");
EditorGUI.indentLevel++;
float lineHeight = EditorGUIUtility.singleLineHeight;
float spacing = 2f;
float y = position.y;
var currentTransition = GetTransition(transition);
if (isChild && currentTransition == Selectable.Transition.Animation)
{
Rect warningRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.HelpBox(warningRect, "Animation 过渡仅允许用于 Main Transition子 Transition 不支持 Animation已显示为 ColorTint", MessageType.Warning);
y += lineHeight + spacing;
currentTransition = Selectable.Transition.ColorTint;
}
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
case Selectable.Transition.SpriteSwap:
Rect targetRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.PropertyField(targetRect, targetGraphic);
y += lineHeight + spacing;
break;
}
Rect transitionRect = new Rect(position.x, y, position.width, lineHeight);
if (isChild)
{
string[] options = new[] { "None", "Color Tint", "Sprite Swap" };
int[] values = new[] { (int)Selectable.Transition.None, (int)Selectable.Transition.ColorTint, (int)Selectable.Transition.SpriteSwap };
int curVal = transition.enumValueIndex;
int selIdx = Array.IndexOf(values, curVal);
if (selIdx < 0) selIdx = 0;
selIdx = EditorGUI.Popup(transitionRect, "Transition", selIdx, options);
transition.enumValueIndex = values[selIdx];
currentTransition = GetTransition(transition);
y += lineHeight + spacing;
}
else
{
EditorGUI.PropertyField(transitionRect, transition);
y += lineHeight + spacing;
}
var graphic = targetGraphic.objectReferenceValue as Graphic;
var animator = graphic != null ? graphic.GetComponent<Animator>() : null;
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
if (graphic == null)
{
Rect warningRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.HelpBox(warningRect, "需要Graphic组件来使用颜色过渡", MessageType.Warning);
y += lineHeight + spacing;
}
break;
case Selectable.Transition.SpriteSwap:
if (!(graphic is Image))
{
Rect warningRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.HelpBox(warningRect, "需要Image组件来使用精灵切换", MessageType.Warning);
y += lineHeight + spacing;
}
break;
case Selectable.Transition.Animation:
if (animator == null)
{
Rect warningRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.HelpBox(warningRect, "需要Animator组件来使用动画切换", MessageType.Warning);
y += lineHeight + spacing;
}
break;
}
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
CheckAndSetColorDefaults(colorBlock, targetGraphic);
Rect colorRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(colorBlock));
EditorGUI.PropertyField(colorRect, colorBlock);
break;
case Selectable.Transition.SpriteSwap:
CheckAndSetColorDefaults(colorBlock, targetGraphic);
Rect spriteRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(spriteState));
EditorGUI.PropertyField(spriteRect, spriteState);
break;
case Selectable.Transition.Animation:
Rect animRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(animationTriggers));
EditorGUI.PropertyField(animRect, animationTriggers);
break;
}
if (graphic != null && currentTransition != (Selectable.Transition)transition.enumValueIndex &&
((Selectable.Transition)transition.enumValueIndex == Selectable.Transition.Animation ||
(Selectable.Transition)transition.enumValueIndex == Selectable.Transition.None))
{
graphic.canvasRenderer.SetColor(Color.white);
}
EditorGUI.indentLevel--;
}
}

3
Editor/UX/Group.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21c6f15c77fa4c6abbe65350c76028a4
timeCreated: 1765184506

View File

@ -0,0 +1,153 @@
using AlicizaX.Editor;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace UnityEngine.UI
{
[CustomEditor(typeof(UXGroup))]
public class UXGroupInspector : GameFrameworkInspector
{
private SerializedProperty m_Buttons;
private UXGroup _target;
private ReorderableList _reorderableList;
private void OnEnable()
{
_target = (UXGroup)target;
m_Buttons = serializedObject.FindProperty("m_Buttons");
_reorderableList = new ReorderableList(serializedObject, m_Buttons, true, true, true, true)
{
drawHeaderCallback = DrawHeader,
drawElementCallback = DrawElement,
onAddCallback = OnAddList,
onRemoveCallback = OnRemoveList,
onChangedCallback = OnChanged,
};
}
public override void OnInspectorGUI()
{
serializedObject.Update();
bool isPlaying = Application.isPlaying || EditorApplication.isPlaying;
_reorderableList.draggable = !isPlaying;
_reorderableList.displayAdd = !isPlaying;
_reorderableList.displayRemove = !isPlaying;
bool prevEnabled = GUI.enabled;
if (isPlaying) GUI.enabled = false;
_reorderableList.DoLayoutList();
GUI.enabled = prevEnabled;
serializedObject.ApplyModifiedProperties();
}
private void DrawHeader(Rect rect)
{
EditorGUI.LabelField(rect, "Toggles", EditorStyles.boldLabel);
}
// 记录旧的引用用于侦测变化
private UXButton previousRef;
private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
SerializedProperty element = m_Buttons.GetArrayElementAtIndex(index);
rect.y += 2;
Rect fieldRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight);
UXButton oldButton = element.objectReferenceValue as UXButton;
string label = $"[{index}] {(oldButton != null ? oldButton.name : "Null")}";
EditorGUI.BeginChangeCheck();
var newRef = EditorGUI.ObjectField(fieldRect, label, oldButton, typeof(UXButton), true) as UXButton;
if (EditorGUI.EndChangeCheck())
{
// 先处理 Remove旧值存在且不同
if (oldButton != null && oldButton != newRef)
{
OnRemove(oldButton);
}
// 再处理 Add新值非空
if (newRef != null && oldButton != newRef)
{
OnAdd(newRef);
}
// 最后把引用写回去
element.objectReferenceValue = newRef;
}
}
private void OnAddList(ReorderableList list)
{
int newIndex = m_Buttons.arraySize;
m_Buttons.arraySize++;
serializedObject.ApplyModifiedProperties();
var newElem = m_Buttons.GetArrayElementAtIndex(newIndex);
newElem.objectReferenceValue = null;
serializedObject.ApplyModifiedProperties();
}
private void OnRemoveList(ReorderableList list)
{
if (list.index < 0 || list.index >= m_Buttons.arraySize)
return;
var oldButton = m_Buttons.GetArrayElementAtIndex(list.index).objectReferenceValue as UXButton;
if (oldButton)
{
OnRemove(oldButton);
}
m_Buttons.DeleteArrayElementAtIndex(list.index);
serializedObject.ApplyModifiedProperties();
}
private void OnChanged(ReorderableList list)
{
serializedObject.ApplyModifiedProperties();
}
// ========================
// 你的新增方法:自动调用
// ========================
private void OnAdd(UXButton button)
{
SerializedObject so = new SerializedObject(button);
var groupProp = so.FindProperty("m_UXGroup");
groupProp.objectReferenceValue = target;
UXGroup group = (UXGroup)target;
group.RegisterButton(button);
so.ApplyModifiedProperties();
}
private void OnRemove(UXButton button)
{
SerializedObject so = new SerializedObject(button);
var groupProp = so.FindProperty("m_UXGroup");
UXGroup group = groupProp.objectReferenceValue as UXGroup;
if (group != null)
group.UnregisterButton(button);
groupProp.objectReferenceValue = null;
so.ApplyModifiedProperties();
}
}
}

View File

@ -8,7 +8,7 @@ using AlicizaX.UI.Runtime;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
namespace UnityEngine.UI.Hotkey namespace UnityEngine.UI
{ {
[CustomEditor(typeof(HotkeyBindComponent))] [CustomEditor(typeof(HotkeyBindComponent))]
public class HotkeyBindComponentInspector : GameFrameworkInspector public class HotkeyBindComponentInspector : GameFrameworkInspector
@ -71,9 +71,9 @@ namespace UnityEngine.UI.Hotkey
for (int i = 0; i < listProp.arraySize; i++) for (int i = 0; i < listProp.arraySize; i++)
{ {
var element = listProp.GetArrayElementAtIndex(i); var element = listProp.GetArrayElementAtIndex(i);
var uxHotkey = element.objectReferenceValue as UXHotkey; var uxHotkey = element.objectReferenceValue as UXHotkeyButton;
string name = uxHotkey != null ? uxHotkey.name : "Null"; string name = uxHotkey != null ? uxHotkey.name : "Null";
EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXHotkey), true); EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXHotkeyButton), true);
} }
EditorGUI.indentLevel--; EditorGUI.indentLevel--;
@ -86,8 +86,8 @@ namespace UnityEngine.UI.Hotkey
{ {
Undo.RecordObject(_target, "Scan UXHotkey"); Undo.RecordObject(_target, "Scan UXHotkey");
var uxHotkeys = _target.GetComponentsInChildren<UXHotkey>(true); var uxHotkeys = _target.GetComponentsInChildren<UXHotkeyButton>(true);
List<UXHotkey> valiedHotkeys = new List<UXHotkey>(); List<UXHotkeyButton> valiedHotkeys = new List<UXHotkeyButton>();
foreach (var item in uxHotkeys) foreach (var item in uxHotkeys)
{ {
var field = item.GetType() var field = item.GetType()

View File

@ -1,45 +0,0 @@
#if INPUTSYSTEM_SUPPORT
using AlicizaX.Editor;
using AlicizaX.UI.Extension;
using UnityEditor;
namespace UnityEngine.UI.Hotkey
{
[CustomEditor(typeof(UXHotkey))]
public class HotkeyInspector : GameFrameworkInspector
{
private UXHotkey _target;
private SerializedProperty _button;
private SerializedProperty _hotKeyRefrence;
private SerializedProperty _hotkeyPressType;
private void OnEnable()
{
_target = (UXHotkey)target;
_button = serializedObject.FindProperty("_button");
_hotKeyRefrence = serializedObject.FindProperty("_hotKeyRefrence");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
var holder = _target.GetComponent<UXButton>();
if (holder == null)
{
EditorGUILayout.HelpBox(
"⚠ 当前对象缺少 UXButton 组件",
MessageType.Error
);
}
EditorGUILayout.PropertyField(_button);
EditorGUILayout.PropertyField(_hotKeyRefrence);
EditorGUILayout.PropertyField(_hotkeyPressType);
serializedObject.ApplyModifiedProperties();
}
}
}
#endif

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 87a10dc3a1544ec586ff27809213a127
timeCreated: 1760341800

View File

@ -0,0 +1,31 @@
using AlicizaX.Editor;
using AlicizaX.UI.Extension;
using UnityEditor;
namespace UnityEngine.UI
{
[CanEditMultipleObjects]
[CustomEditor(typeof(UXHotkeyButton), true)]
internal class UXHotkeyButtonEditor : UXButtonEditor
{
private SerializedProperty _hotKeyRefrence;
private SerializedProperty _hotkeyPressType;
protected override void OnEnable()
{
base.OnEnable();
_hotKeyRefrence = serializedObject.FindProperty("_hotKeyRefrence");
_hotkeyPressType = serializedObject.FindProperty("_hotkeyPressType");
}
protected override void DrawEventTab()
{
base.DrawEventTab();
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
{
EditorGUILayout.PropertyField(_hotKeyRefrence, new GUIContent("InputAction"));
EditorGUILayout.PropertyField(_hotkeyPressType, new GUIContent("PressType"));
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dc0b010e4f884f6992de90c5e5dd7154
timeCreated: 1765185454

3
Editor/UX/Iamge.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 681126dbfd104770895c5b2dedb80aa0
timeCreated: 1765184491

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 29e9949ea8274c14b76e93d316a2d101
timeCreated: 1765184529

View File

@ -0,0 +1,570 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AlicizaX.UI.Extension;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;
using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType;
namespace UnityEngine.UI
{
[CanEditMultipleObjects]
[CustomEditor(typeof(UXSelectable), true)]
internal class UXSelectableEditor : Editor
{
private static bool s_ShowNavigation = false;
public static string s_ShowNavigationKey = "SelectableEditor.ShowNavigation";
GUIContent m_VisualizeNavigation = EditorGUIUtility.TrTextContent("Visualize", "Show navigation flows between selectable UI elements.");
private static List<UXSelectableEditor> s_Editors = new List<UXSelectableEditor>();
// 序列化属性(基类需要)
protected SerializedProperty m_Navigation;
protected SerializedProperty m_MainTransition;
protected SerializedProperty m_Interactable;
// 抽象皮肤(现在放在基类)
protected GUISkin customSkin;
// Tab 管理结构
protected class EditorTab
{
public string title;
public string iconName;
public List<Action> callbacks = new List<Action>();
public EditorTab(string t, string icon)
{
title = t;
iconName = icon;
}
}
private List<EditorTab> _tabs = new List<EditorTab>();
private int _currentTabIndex = 0;
protected virtual void OnEnable()
{
s_ShowNavigation = EditorPrefs.GetBool(s_ShowNavigationKey);
s_Editors.Add(this);
RegisterStaticOnSceneGUI();
m_Navigation = serializedObject.FindProperty("m_Navigation");
m_MainTransition = serializedObject.FindProperty("m_MainTransition");
m_Interactable = serializedObject.FindProperty("m_Interactable");
// load customSkin once in base; 调整路径为你项目中 GUISkin 的实际路径(必要时修改)
customSkin = AssetDatabase.LoadAssetAtPath<GUISkin>("Packages/com.alicizax.unity.ui.extension/Editor/Res/GUISkin/UIExtensionGUISkin.guiskin");
// Ensure default Image tab exists and contains DrawSelectableInspector
EnsureDefaultImageTab();
}
protected virtual void OnDisable()
{
s_Editors.Remove(this);
RegisterStaticOnSceneGUI();
_tabs.Clear();
_currentTabIndex = 0;
}
#region Tab API (Register / Append / Remove / Unregister)
/// <summary>
/// 注册一个新 tab如果已存在同名 tab则替换其 icon 和清空回调,再加入 drawCallback
/// </summary>
protected void RegisterTab(string title, string iconName, Action drawCallback)
{
if (string.IsNullOrEmpty(title)) return;
var tab = _tabs.Find(t => t.title == title);
if (tab == null)
{
tab = new EditorTab(title, iconName);
_tabs.Add(tab);
}
else
{
tab.iconName = iconName;
tab.callbacks.Clear();
}
if (drawCallback != null)
tab.callbacks.Add(drawCallback);
}
/// <summary>
/// 在已存在 tab 后追加一个 callback若 tab 不存在则创建并追加。
/// </summary>
protected void AppendToTab(string title, Action drawCallback, bool last = true)
{
if (string.IsNullOrEmpty(title) || drawCallback == null) return;
var tab = _tabs.Find(t => t.title == title);
if (tab == null)
{
tab = new EditorTab(title, "d_DefaultAsset Icon");
_tabs.Add(tab);
}
// 避免重复追加(简单判断)
if (!tab.callbacks.Contains(drawCallback))
{
if (last)
{
tab.callbacks.Add(drawCallback);
}
else
{
tab.callbacks.Insert(0, drawCallback);
}
}
}
/// <summary>
/// 从指定 tab 中移除某个 callback子类在 OnDisable 应该调用以清理)
/// </summary>
protected void RemoveCallbackFromTab(string title, Action drawCallback)
{
if (string.IsNullOrEmpty(title) || drawCallback == null) return;
var tab = _tabs.Find(t => t.title == title);
if (tab == null) return;
tab.callbacks.RemoveAll(cb => cb == drawCallback);
}
/// <summary>
/// 卸载整个 tab移除该 title 的所有 callbacks 与 tab 本身)。
/// 如果当前选中的索引超出范围,会自动修正为合法值。
/// 注意:基类默认创建的 "Image" 页签通常不应被移除,若需要保护可以在此加判断。
/// </summary>
protected void UnregisterTab(string title)
{
if (string.IsNullOrEmpty(title)) return;
_tabs.RemoveAll(t => t.title == title);
// 修正当前索引,避免越界
if (_tabs.Count == 0)
{
_currentTabIndex = 0;
}
else if (_currentTabIndex >= _tabs.Count)
{
_currentTabIndex = Mathf.Max(0, _tabs.Count - 1);
}
}
private void EnsureDefaultImageTab()
{
// 如果还没有 Image tab创建并把 DrawSelectableInspector 放到第一个 callback
var tab = _tabs.Find(t => t.title == "Image");
if (tab == null)
{
tab = new EditorTab("Image", "d_Texture Icon");
_tabs.Insert(0, tab);
}
// 确保基类绘制在 Image 页签的第一个 callback (避免重复)
if (!tab.callbacks.Contains(DrawSelectableInspector))
tab.callbacks.Insert(0, DrawSelectableInspector);
}
#endregion
protected void DrawToggleShowNavigation()
{
Rect toggleRect = EditorGUILayout.GetControlRect();
toggleRect.xMin += EditorGUIUtility.labelWidth;
EditorGUI.BeginChangeCheck();
s_ShowNavigation = GUI.Toggle(toggleRect, s_ShowNavigation, m_VisualizeNavigation, EditorStyles.miniButton);
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool(s_ShowNavigationKey, s_ShowNavigation);
SceneView.RepaintAll();
}
}
#region Scene GUI visualization (unchanged)
private void RegisterStaticOnSceneGUI()
{
SceneView.duringSceneGui -= StaticOnSceneGUI;
if (s_Editors.Count > 0)
SceneView.duringSceneGui += StaticOnSceneGUI;
}
private static void StaticOnSceneGUI(SceneView view)
{
if (!s_ShowNavigation)
return;
UXSelectable[] selectables = UXSelectable.allSelectablesArray;
for (int i = 0; i < selectables.Length; i++)
{
UXSelectable s = selectables[i];
if (StageUtility.IsGameObjectRenderedByCamera(s.gameObject, Camera.current))
DrawNavigationForSelectable(s);
}
}
private static void DrawNavigationForSelectable(UXSelectable sel)
{
if (sel == null)
return;
Transform transform = sel.transform;
bool active = Selection.transforms.Any(e => e == transform);
Handles.color = new Color(1.0f, 0.6f, 0.2f, active ? 1.0f : 0.4f);
DrawNavigationArrow(-Vector2.right, sel, sel.FindSelectableOnLeft());
DrawNavigationArrow(Vector2.up, sel, sel.FindSelectableOnUp());
Handles.color = new Color(1.0f, 0.9f, 0.1f, active ? 1.0f : 0.4f);
DrawNavigationArrow(Vector2.right, sel, sel.FindSelectableOnRight());
DrawNavigationArrow(-Vector2.up, sel, sel.FindSelectableOnDown());
}
const float kArrowThickness = 2.5f;
const float kArrowHeadSize = 1.2f;
private static void DrawNavigationArrow(Vector2 direction, UXSelectable fromObj, UXSelectable toObj)
{
if (fromObj == null || toObj == null)
return;
Transform fromTransform = fromObj.transform;
Transform toTransform = toObj.transform;
Vector2 sideDir = new Vector2(direction.y, -direction.x);
Vector3 fromPoint = fromTransform.TransformPoint(GetPointOnRectEdge(fromTransform as RectTransform, direction));
Vector3 toPoint = toTransform.TransformPoint(GetPointOnRectEdge(toTransform as RectTransform, -direction));
float fromSize = HandleUtility.GetHandleSize(fromPoint) * 0.05f;
float toSize = HandleUtility.GetHandleSize(toPoint) * 0.05f;
fromPoint += fromTransform.TransformDirection(sideDir) * fromSize;
toPoint += toTransform.TransformDirection(sideDir) * toSize;
float length = Vector3.Distance(fromPoint, toPoint);
Vector3 fromTangent = fromTransform.rotation * direction * length * 0.3f;
Vector3 toTangent = toTransform.rotation * -direction * length * 0.3f;
Handles.DrawBezier(fromPoint, toPoint, fromPoint + fromTangent, toPoint + toTangent, Handles.color, null, kArrowThickness);
Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction - sideDir) * toSize * kArrowHeadSize);
Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction + sideDir) * toSize * kArrowHeadSize);
}
private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir)
{
if (rect == null)
return Vector3.zero;
if (dir != Vector2.zero)
dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y));
dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f);
return dir;
}
#endregion
/// <summary>
/// 子类调用:绘制基类的通用 InspectorNavigation / Interactable / Main Transition
/// </summary>
protected void DrawSelectableInspector()
{
if (m_Navigation != null)
EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_Mode"), new GUIContent("Navigation"));
DrawToggleShowNavigation();
if (customSkin != null)
{
var interactable = GUILayoutHelper.DrawToggle(m_Interactable.boolValue, customSkin, "Interactable");
if (interactable != m_Interactable.boolValue)
{
(target as UXSelectable).Interactable = interactable;
m_Interactable.boolValue = interactable;
// keep selection state consistent
var selProp = serializedObject.FindProperty("m_SelectionState");
if (selProp != null)
{
selProp.enumValueIndex = interactable ? (int)UXSelectable.SelectionState.Normal : (int)UXSelectable.SelectionState.Disabled;
}
}
}
GUILayout.Space(1);
serializedObject.ApplyModifiedProperties();
DrawSelfTransition();
GUILayout.Space(5);
}
#region MainTransition helpers (moved to base)
protected virtual void DrawSelfTransition()
{
if (m_MainTransition == null)
return;
GUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Main Transition", EditorStyles.boldLabel);
SerializedProperty targetGraphic = m_MainTransition.FindPropertyRelative("targetGraphic");
var graphic = targetGraphic.objectReferenceValue as Graphic;
if (graphic == null)
{
graphic = (target as UXSelectable).GetComponent<Graphic>();
if (targetGraphic != null)
targetGraphic.objectReferenceValue = graphic;
}
SerializedProperty transition = m_MainTransition.FindPropertyRelative("transition");
var currentTransition = GetTransition(transition);
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
case Selectable.Transition.SpriteSwap:
EditorGUILayout.PropertyField(targetGraphic);
break;
}
EditorGUILayout.PropertyField(transition);
var animator = graphic != null ? graphic.GetComponent<Animator>() : (target as UXSelectable).GetComponent<Animator>();
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
if (graphic == null)
EditorGUILayout.HelpBox("需要Graphic组件来使用颜色过渡", MessageType.Warning);
break;
case Selectable.Transition.SpriteSwap:
if (!(graphic is Image))
EditorGUILayout.HelpBox("需要Image组件来使用精灵切换", MessageType.Warning);
break;
case Selectable.Transition.Animation:
if (animator == null)
EditorGUILayout.HelpBox("需要Animator组件来使用动画切换", MessageType.Warning);
break;
}
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
CheckAndSetColorDefaults(m_MainTransition.FindPropertyRelative("colors"), targetGraphic);
EditorGUILayout.PropertyField(m_MainTransition.FindPropertyRelative("colors"));
break;
case Selectable.Transition.SpriteSwap:
EditorGUILayout.PropertyField(m_MainTransition.FindPropertyRelative("spriteState"));
break;
case Selectable.Transition.Animation:
EditorGUILayout.PropertyField(m_MainTransition.FindPropertyRelative("animationTriggers"));
if (animator == null || animator.runtimeAnimatorController == null)
{
if (GUILayout.Button("Auto Generate Animation"))
{
var animationTrigger = m_MainTransition.FindPropertyRelative("animationTriggers");
var controller = GenerateSelectableAnimatorContoller(animationTrigger, target);
if (controller != null)
{
if (animator == null)
animator = (target as UXSelectable).gameObject.AddComponent<Animator>();
UnityEditor.Animations.AnimatorController.SetAnimatorController(animator, controller);
}
}
}
break;
}
if (graphic != null && currentTransition != (Selectable.Transition)transition.enumValueIndex &&
((Selectable.Transition)transition.enumValueIndex == Selectable.Transition.Animation ||
(Selectable.Transition)transition.enumValueIndex == Selectable.Transition.None))
{
graphic.canvasRenderer.SetColor(Color.white);
}
EditorGUILayout.Space();
GUILayout.EndVertical();
}
protected void CheckAndSetColorDefaults(SerializedProperty colorBlock, SerializedProperty targetGraphic)
{
if (colorBlock == null) return;
bool isDirty = false;
string[] colorProps = new string[] { "m_NormalColor", "m_HighlightedColor", "m_PressedColor", "m_SelectedColor", "m_DisabledColor" };
foreach (var propName in colorProps)
{
SerializedProperty prop = colorBlock.FindPropertyRelative(propName);
if (prop == null) continue;
Color color = prop.colorValue;
if (color.r == 0 && color.g == 0 && color.b == 0 && color.a == 0)
{
isDirty = true;
if (prop.name == "m_PressedColor")
{
prop.colorValue = new Color(0.7843137f, 0.7843137f, 0.7843137f, 1.0f);
}
else if (prop.name == "m_DisabledColor")
{
prop.colorValue = new Color(0.7843137f, 0.7843137f, 0.7843137f, 0.5f);
}
else
{
prop.colorValue = Color.white;
}
}
}
SerializedProperty fadeDuration = colorBlock.FindPropertyRelative("m_FadeDuration");
SerializedProperty m_ColorMultiplier = colorBlock.FindPropertyRelative("m_ColorMultiplier");
if (isDirty)
{
if (m_ColorMultiplier != null) m_ColorMultiplier.floatValue = 1f;
if (fadeDuration != null) fadeDuration.floatValue = 0.1f;
}
var graphic = targetGraphic != null ? targetGraphic.objectReferenceValue as Graphic : null;
if (graphic != null)
{
if (!EditorApplication.isPlaying)
{
SerializedProperty normalColorProp = colorBlock.FindPropertyRelative("m_NormalColor");
if (normalColorProp != null)
{
Color color = normalColorProp.colorValue;
graphic.canvasRenderer.SetColor(color);
}
}
}
}
protected static UnityEditor.Animations.AnimatorController GenerateSelectableAnimatorContoller(SerializedProperty property, UnityEngine.Object targetObj)
{
if (targetObj == null || property == null)
return null;
var targetAsGO = (targetObj as UXSelectable);
var path = GetSaveControllerPath(targetAsGO);
if (string.IsNullOrEmpty(path))
return null;
SerializedProperty normalTrigger = property.FindPropertyRelative("m_NormalTrigger");
SerializedProperty highlightedTrigger = property.FindPropertyRelative("m_HighlightedTrigger");
SerializedProperty pressedTrigger = property.FindPropertyRelative("m_PressedTrigger");
SerializedProperty selectedTrigger = property.FindPropertyRelative("m_SelectedTrigger");
SerializedProperty disabledTrigger = property.FindPropertyRelative("m_DisabledTrigger");
var normalName = string.IsNullOrEmpty(normalTrigger.stringValue) ? "Normal" : normalTrigger.stringValue;
var highlightedName = string.IsNullOrEmpty(highlightedTrigger.stringValue) ? "Highlighted" : highlightedTrigger.stringValue;
var pressedName = string.IsNullOrEmpty(pressedTrigger.stringValue) ? "Pressed" : pressedTrigger.stringValue;
var selectedName = string.IsNullOrEmpty(selectedTrigger.stringValue) ? "Selected" : selectedTrigger.stringValue;
var disabledName = string.IsNullOrEmpty(disabledTrigger.stringValue) ? "Disabled" : disabledTrigger.stringValue;
var controller = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(path);
GenerateTriggerableTransition(normalName, controller);
GenerateTriggerableTransition(highlightedName, controller);
GenerateTriggerableTransition(pressedName, controller);
GenerateTriggerableTransition(selectedName, controller);
GenerateTriggerableTransition(disabledName, controller);
AssetDatabase.ImportAsset(path);
return controller;
}
private static AnimationClip GenerateTriggerableTransition(string name, UnityEditor.Animations.AnimatorController controller)
{
// Create the clip
var clip = UnityEditor.Animations.AnimatorController.AllocateAnimatorClip(name);
AssetDatabase.AddObjectToAsset(clip, controller);
// Create a state in the animatior controller for this clip
var state = controller.AddMotion(clip);
// Add a transition property
controller.AddParameter(name, AnimatorControllerParameterType.Trigger);
// Add an any state transition
var stateMachine = controller.layers[0].stateMachine;
var transition = stateMachine.AddAnyStateTransition(state);
transition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, name);
return clip;
}
private static string GetSaveControllerPath(UXSelectable target)
{
var defaultName = target != null ? target.gameObject.name : "NewController";
var message = string.Format("Create a new animator for the game object '{0}':", defaultName);
return EditorUtility.SaveFilePanelInProject("New Animation Contoller", defaultName, "controller", message);
}
protected static Selectable.Transition GetTransition(SerializedProperty transition)
{
if (transition == null) return Selectable.Transition.None;
return (Selectable.Transition)transition.enumValueIndex;
}
#endregion
#region Tab drawing entry
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawTabs();
serializedObject.ApplyModifiedProperties();
}
protected void DrawTabs()
{
if (_tabs == null || _tabs.Count == 0)
{
// fallback: draw base directly
DrawSelectableInspector();
return;
}
EditorGUILayout.BeginHorizontal();
for (int i = 0; i < _tabs.Count; i++)
{
var tab = _tabs[i];
bool isActive = (i == _currentTabIndex);
var style = new GUIStyle(EditorStyles.toolbarButton) { fixedHeight = 25, fontSize = 11, fontStyle = isActive ? FontStyle.Bold : FontStyle.Normal };
var content = new GUIContent(EditorGUIUtility.IconContent(tab.iconName).image, tab.title);
if (GUILayout.Button(content, style))
{
_currentTabIndex = i;
}
if (isActive)
{
Rect rect = GUILayoutUtility.GetLastRect();
EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 2, rect.width, 2), new Color(0.1f, 0.5f, 0.9f));
}
}
EditorGUILayout.EndHorizontal();
GUILayout.Space(4);
// draw current tab content (call all callbacks registered for the tab)
var callbacks = _tabs[_currentTabIndex].callbacks;
if (callbacks != null)
{
foreach (var cb in callbacks)
{
try
{
cb?.Invoke();
}
catch (UnityEngine.ExitGUIException)
{
// Unity 用 ExitGUIException 来中断 GUI 流程 —— 需要重新抛出,让 Unity 处理
throw;
}
catch (Exception e)
{
// 仅记录真正的异常
Debug.LogException(e);
}
}
}
}
#endregion
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1e63c9ae25da41d482430905b232d091
timeCreated: 1765183957

3
Editor/UX/Slider.meta Normal file
View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 708a130e1d5f49f18619c6ad9c3baccb
timeCreated: 1765260889

View File

@ -0,0 +1,158 @@
using UnityEditor;
using UnityEditor.UI;
namespace UnityEngine.UI
{
[CustomEditor(typeof(UXSlider), true)]
[CanEditMultipleObjects]
internal class UXSliderEditor : UXSelectableEditor
{
SerializedProperty m_Direction;
SerializedProperty m_FillRect;
SerializedProperty m_HandleRect;
SerializedProperty m_MinValue;
SerializedProperty m_MaxValue;
SerializedProperty m_WholeNumbers;
SerializedProperty m_Value;
SerializedProperty m_OnValueChanged;
SerializedProperty m_SmoothMovement;
SerializedProperty m_SmoothSpeed;
protected override void OnEnable()
{
base.OnEnable();
m_FillRect = serializedObject.FindProperty("m_FillRect");
m_HandleRect = serializedObject.FindProperty("m_HandleRect");
m_Direction = serializedObject.FindProperty("m_Direction");
m_MinValue = serializedObject.FindProperty("m_MinValue");
m_MaxValue = serializedObject.FindProperty("m_MaxValue");
m_WholeNumbers = serializedObject.FindProperty("m_WholeNumbers");
m_Value = serializedObject.FindProperty("m_Value");
m_OnValueChanged = serializedObject.FindProperty("m_OnValueChanged");
m_SmoothMovement = serializedObject.FindProperty("m_SmoothMovement");
m_SmoothSpeed = serializedObject.FindProperty("m_SmoothSpeed");
AppendToTab("Image", DrawImage);
}
protected override void OnDisable()
{
RemoveCallbackFromTab("Image", DrawImage);
base.OnDisable();
}
private void DrawImage()
{
EditorGUILayout.PropertyField(m_FillRect);
EditorGUILayout.PropertyField(m_HandleRect);
if (m_FillRect.objectReferenceValue != null || m_HandleRect.objectReferenceValue != null)
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(m_Direction);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObjects(serializedObject.targetObjects, "Change Slider Direction");
Slider.Direction direction = (Slider.Direction)m_Direction.enumValueIndex;
foreach (var obj in serializedObject.targetObjects)
{
UXSlider slider = obj as UXSlider;
slider.SetDirection(direction, true);
}
}
EditorGUI.BeginChangeCheck();
float newMin = EditorGUILayout.FloatField("Min Value", m_MinValue.floatValue);
if (EditorGUI.EndChangeCheck())
{
if (m_WholeNumbers.boolValue ? Mathf.Round(newMin) < m_MaxValue.floatValue : newMin < m_MaxValue.floatValue)
{
m_MinValue.floatValue = newMin;
if (m_Value.floatValue < newMin)
m_Value.floatValue = newMin;
}
}
EditorGUI.BeginChangeCheck();
float newMax = EditorGUILayout.FloatField("Max Value", m_MaxValue.floatValue);
if (EditorGUI.EndChangeCheck())
{
if (m_WholeNumbers.boolValue ? Mathf.Round(newMax) > m_MinValue.floatValue : newMax > m_MinValue.floatValue)
{
m_MaxValue.floatValue = newMax;
if (m_Value.floatValue > newMax)
m_Value.floatValue = newMax;
}
}
EditorGUILayout.PropertyField(m_WholeNumbers);
bool areMinMaxEqual = (m_MinValue.floatValue == m_MaxValue.floatValue);
if (areMinMaxEqual)
EditorGUILayout.HelpBox("Min Value and Max Value cannot be equal.", MessageType.Warning);
if (m_WholeNumbers.boolValue)
m_Value.floatValue = Mathf.Round(m_Value.floatValue);
EditorGUI.BeginDisabledGroup(areMinMaxEqual);
EditorGUI.BeginChangeCheck();
EditorGUILayout.Slider(m_Value, m_MinValue.floatValue, m_MaxValue.floatValue);
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
foreach (var t in targets)
{
if (t is UXSlider slider)
{
slider.onValueChanged?.Invoke(slider.value);
}
}
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Move Smoothing (Gamepad / Keys)", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_SmoothMovement, new GUIContent("Enable Smooth Movement"));
using (new EditorGUI.DisabledScope(m_SmoothMovement.boolValue == false))
{
EditorGUILayout.PropertyField(m_SmoothSpeed, new GUIContent("Smooth Speed (value/sec)"));
if (m_WholeNumbers.boolValue && m_SmoothMovement.boolValue)
{
EditorGUILayout.HelpBox("Note: wholeNumbers is enabled — smooth movement will be disabled at runtime (values stay integer).", MessageType.Info);
}
}
bool warning = false;
foreach (var obj in serializedObject.targetObjects)
{
UXSlider slider = obj as UXSlider;
Slider.Direction dir = slider.direction;
if (dir == Slider.Direction.LeftToRight || dir == Slider.Direction.RightToLeft)
warning = (slider.navigation.mode != UXNavigation.Mode.Automatic && (slider.FindSelectableOnLeft() != null || slider.FindSelectableOnRight() != null));
else
warning = (slider.navigation.mode != UXNavigation.Mode.Automatic && (slider.FindSelectableOnDown() != null || slider.FindSelectableOnUp() != null));
}
if (warning)
EditorGUILayout.HelpBox("The selected slider direction conflicts with navigation. Not all navigation options may work.", MessageType.Warning);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_OnValueChanged);
}
else
{
EditorGUILayout.HelpBox("Specify a RectTransform for the slider fill or the slider handle or both. Each must have a parent RectTransform that it can slide within.", MessageType.Info);
}
serializedObject.ApplyModifiedProperties();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e67f2a4d5c0a4f399f67374dcfd42a1f
timeCreated: 1765260896

View File

@ -1,732 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AlicizaX.UI.Extension;
using AlicizaX.UI.Extension.Editor;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEditor.UI;
using UnityEngine;
using UnityEngine.UI;
using AnimatorControllerParameterType = UnityEngine.AnimatorControllerParameterType;
[CanEditMultipleObjects]
[CustomEditor(typeof(UXButton), true)]
internal class UXButtonEditor : Editor
{
private enum TabType
{
Image,
Sound,
Event
}
GUIContent m_VisualizeNavigation = EditorGUIUtility.TrTextContent("Visualize", "Show navigation flows between selectable UI elements.");
private static bool s_ShowNavigation = false;
private static string s_ShowNavigationKey = "SelectableEditor.ShowNavigation";
private static List<UXButtonEditor> s_Editors = new List<UXButtonEditor>();
private SerializedProperty m_Interactable;
private SerializedProperty m_Mode;
private SerializedProperty m_OnValueChanged;
private SerializedProperty m_OnClick;
private SerializedProperty m_UXGroup;
private SerializedProperty m_TransitionData;
private SerializedProperty m_ChildTransitions;
private SerializedProperty m_SelectionState;
private ReorderableList m_ChildTransitionList;
private static Color darkZebraEven = new Color(0.22f, 0.22f, 0.22f);
private static Color darkZebraOdd = new Color(0.27f, 0.27f, 0.27f);
private TabType currentTab = TabType.Image;
private GUISkin customSkin;
private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip;
private SerializedProperty m_Navigation;
private UXButton mTarget;
private void OnEnable()
{
mTarget = (UXButton)target;
customSkin = AssetDatabase.LoadAssetAtPath<GUISkin>("Packages/com.alicizax.unity.ui.extension/Editor/Res/GUISkin/UIExtensionGUISkin.guiskin");
m_Interactable = serializedObject.FindProperty("m_Interactable");
m_UXGroup = serializedObject.FindProperty("m_UXGroup");
m_Mode = serializedObject.FindProperty("m_Mode");
m_OnValueChanged = serializedObject.FindProperty("m_OnValueChanged");
m_OnClick = serializedObject.FindProperty("m_OnClick");
m_TransitionData = serializedObject.FindProperty("m_TransitionData");
m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions");
m_SelectionState = serializedObject.FindProperty("m_SelectionState");
m_Navigation = serializedObject.FindProperty("m_Navigation");
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
clickAudioClip = serializedObject.FindProperty("clickAudioClip");
CreateChildTransitionList();
s_Editors.Add(this);
RegisterStaticOnSceneGUI();
s_ShowNavigation = EditorPrefs.GetBool(s_ShowNavigationKey);
}
protected virtual void OnDisable()
{
s_Editors.Remove(this);
RegisterStaticOnSceneGUI();
}
#region Navigation
private void RegisterStaticOnSceneGUI()
{
SceneView.duringSceneGui -= StaticOnSceneGUI;
if (s_Editors.Count > 0)
SceneView.duringSceneGui += StaticOnSceneGUI;
}
private static void StaticOnSceneGUI(SceneView view)
{
if (!s_ShowNavigation)
return;
UXSelectable[] selectables = UXSelectable.allSelectablesArray;
for (int i = 0; i < selectables.Length; i++)
{
UXSelectable s = selectables[i];
if (StageUtility.IsGameObjectRenderedByCamera(s.gameObject, Camera.current))
DrawNavigationForSelectable(s);
}
}
private static void DrawNavigationForSelectable(UXSelectable sel)
{
if (sel == null)
return;
Transform transform = sel.transform;
bool active = Selection.transforms.Any(e => e == transform);
Handles.color = new Color(1.0f, 0.6f, 0.2f, active ? 1.0f : 0.4f);
DrawNavigationArrow(-Vector2.right, sel, sel.FindSelectableOnLeft());
DrawNavigationArrow(Vector2.up, sel, sel.FindSelectableOnUp());
Handles.color = new Color(1.0f, 0.9f, 0.1f, active ? 1.0f : 0.4f);
DrawNavigationArrow(Vector2.right, sel, sel.FindSelectableOnRight());
DrawNavigationArrow(-Vector2.up, sel, sel.FindSelectableOnDown());
}
const float kArrowThickness = 2.5f;
const float kArrowHeadSize = 1.2f;
private static void DrawNavigationArrow(Vector2 direction, UXSelectable fromObj, UXSelectable toObj)
{
if (fromObj == null || toObj == null)
return;
Transform fromTransform = fromObj.transform;
Transform toTransform = toObj.transform;
Vector2 sideDir = new Vector2(direction.y, -direction.x);
Vector3 fromPoint = fromTransform.TransformPoint(GetPointOnRectEdge(fromTransform as RectTransform, direction));
Vector3 toPoint = toTransform.TransformPoint(GetPointOnRectEdge(toTransform as RectTransform, -direction));
float fromSize = HandleUtility.GetHandleSize(fromPoint) * 0.05f;
float toSize = HandleUtility.GetHandleSize(toPoint) * 0.05f;
fromPoint += fromTransform.TransformDirection(sideDir) * fromSize;
toPoint += toTransform.TransformDirection(sideDir) * toSize;
float length = Vector3.Distance(fromPoint, toPoint);
Vector3 fromTangent = fromTransform.rotation * direction * length * 0.3f;
Vector3 toTangent = toTransform.rotation * -direction * length * 0.3f;
Handles.DrawBezier(fromPoint, toPoint, fromPoint + fromTangent, toPoint + toTangent, Handles.color, null, kArrowThickness);
Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction - sideDir) * toSize * kArrowHeadSize);
Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction + sideDir) * toSize * kArrowHeadSize);
}
private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir)
{
if (rect == null)
return Vector3.zero;
if (dir != Vector2.zero)
dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y));
dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f);
return dir;
}
#endregion
private void CreateChildTransitionList()
{
m_ChildTransitionList = new ReorderableList(serializedObject, m_ChildTransitions, true, false, true, true);
// m_ChildTransitionList.drawHeaderCallback = (rect) => { EditorGUI.LabelField(rect, "Other Transitions"); };
m_ChildTransitionList.drawElementBackgroundCallback = (rect, index, isActive, isFocused) =>
{
var background = index % 2 == 0 ? darkZebraEven : darkZebraOdd;
EditorGUI.DrawRect(rect, background);
};
m_ChildTransitionList.drawElementCallback = (rect, index, isActive, isFocused) =>
{
var element = m_ChildTransitionList.serializedProperty.GetArrayElementAtIndex(index);
rect.y += 2;
// 绘制折叠框标题(仅显示名称)
string elementTitle = $"Null Transition";
var targetProp = element.FindPropertyRelative("targetGraphic");
if (targetProp.objectReferenceValue != null)
elementTitle = targetProp.objectReferenceValue.name;
EditorGUI.LabelField(new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight),
elementTitle, EditorStyles.boldLabel);
// 直接绘制完整内容(无折叠状态)
rect.y += EditorGUIUtility.singleLineHeight + 2;
DrawTransitionData(new Rect(rect.x, rect.y, rect.width, 0), element, true);
};
// 设置元素高度
m_ChildTransitionList.elementHeightCallback = (index) =>
{
return EditorGUIUtility.singleLineHeight +
CalculateTransitionDataHeight(m_ChildTransitionList.serializedProperty.GetArrayElementAtIndex(index)) +
10;
};
m_ChildTransitionList.onAddCallback = (list) =>
{
list.serializedProperty.arraySize++;
serializedObject.ApplyModifiedProperties();
};
// 添加删除按钮
m_ChildTransitionList.onRemoveCallback = (list) => { ReorderableList.defaultBehaviours.DoRemoveButton(list); };
}
private void ResetEventProperty(SerializedProperty property)
{
SerializedProperty persistentCalls = property.FindPropertyRelative("m_PersistentCalls");
SerializedProperty calls = persistentCalls.FindPropertyRelative("m_Calls");
calls.arraySize = 0;
property.serializedObject.ApplyModifiedProperties();
}
private void DrawTabButton(TabType tabType, string label, string iconName)
{
bool isActive = currentTab == tabType;
var style = new GUIStyle(EditorStyles.toolbarButton)
{
fixedHeight = 25,
fontSize = 11,
fontStyle = isActive ? FontStyle.Bold : FontStyle.Normal
};
var content = new GUIContent(
EditorGUIUtility.IconContent(iconName).image,
label
);
if (GUILayout.Button(content, style))
{
currentTab = tabType;
}
// 高亮当前选中的标签页
if (isActive)
{
Rect rect = GUILayoutUtility.GetLastRect();
EditorGUI.DrawRect(new Rect(rect.x, rect.yMax - 2, rect.width, 2),
new Color(0.1f, 0.5f, 0.9f));
}
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.BeginHorizontal();
// 图像标签页按钮
DrawTabButton(TabType.Image, "Image", "d_Texture Icon");
// 声音标签页按钮
DrawTabButton(TabType.Sound, "Sound", "d_AudioSource Icon");
// 事件标签页按钮
DrawTabButton(TabType.Event, "Event", "EventTrigger Icon");
EditorGUILayout.EndHorizontal();
switch (currentTab)
{
case TabType.Image:
DrawGraphicsTab();
break;
case TabType.Sound:
DrawAudioTab();
break;
case TabType.Event:
DrawEventTab();
break;
}
serializedObject.ApplyModifiedProperties();
}
private void DrawGraphicsTab()
{
EditorGUI.BeginDisabledGroup(EditorApplication.isPlaying);
var modeType = (ButtonModeType)EditorGUILayout.EnumPopup("Mode", (ButtonModeType)m_Mode.enumValueIndex);
if (modeType != (ButtonModeType)m_Mode.enumValueIndex)
{
if (modeType == ButtonModeType.Normal)
{
ResetEventProperty(m_OnValueChanged);
m_UXGroup.objectReferenceValue = null;
}
else
{
ResetEventProperty(m_OnClick);
}
m_Mode.enumValueIndex = (int)modeType;
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_Mode"),new GUIContent("Navigation"));
Rect toggleRect = EditorGUILayout.GetControlRect();
toggleRect.xMin += EditorGUIUtility.labelWidth;
s_ShowNavigation = GUI.Toggle(toggleRect, s_ShowNavigation, m_VisualizeNavigation, EditorStyles.miniButton);
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool(s_ShowNavigationKey, s_ShowNavigation);
SceneView.RepaintAll();
}
var interactable = GUILayoutHelper.DrawToggle(m_Interactable.boolValue, customSkin, "Interactable");
if (interactable != m_Interactable.boolValue)
{
mTarget.Interactable = interactable;
m_SelectionState.enumValueIndex = interactable ? 0 : 4;
}
m_Interactable.boolValue = interactable;
GUILayout.Space(1);
DrawSelfTransition();
GUILayout.Space(5);
DrawChildTransitions();
GUILayout.Space(1);
DrawBasicSettings();
}
private void DrawEventTab()
{
if (m_Mode.enumValueIndex == (int)ButtonModeType.Toggle)
{
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_OnValueChanged);
}
else
{
EditorGUILayout.Space();
EditorGUILayout.PropertyField(m_OnClick);
}
EditorGUILayout.Separator();
}
private void DrawAudioTab()
{
GUILayoutHelper.DrawProperty(hoverAudioClip, customSkin, "Hover Sound", "Play", () =>
{
if (hoverAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)hoverAudioClip.objectReferenceValue);
}
});
GUILayoutHelper.DrawProperty(clickAudioClip, customSkin, "Click Sound", "Play", () =>
{
if (clickAudioClip.objectReferenceValue != null)
{
PlayAudio((AudioClip)clickAudioClip.objectReferenceValue);
}
});
}
private void DrawBasicSettings()
{
if (m_Mode.enumValueIndex == (int)ButtonModeType.Toggle)
{
GUILayoutHelper.DrawProperty<UXGroup>(m_UXGroup, customSkin, "UXGroup", (oldValue, newValue) =>
{
UXButton self = target as UXButton;
if (oldValue != null)
{
oldValue.UnregisterButton(self);
}
if (newValue != null)
{
newValue.RegisterButton(self);
}
});
}
}
private void PlayAudio(AudioClip clip)
{
if (clip != null)
{
ExtensionHelper.PreviewAudioClip(clip);
}
}
private void DrawChildTransitions()
{
m_ChildTransitionList.DoLayoutList();
}
private float CalculateTransitionDataHeight(SerializedProperty transitionData)
{
float height = 0;
SerializedProperty transition = transitionData.FindPropertyRelative("transition");
var currentTransition = GetTransition(transition);
height += EditorGUIUtility.singleLineHeight * 1.5f;
height += EditorGUIUtility.singleLineHeight;
SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic");
var graphic = targetGraphic.objectReferenceValue as Graphic;
var animator = graphic != null ? graphic.GetComponent<Animator>() : null;
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
if (graphic == null) height += EditorGUIUtility.singleLineHeight;
break;
case Selectable.Transition.SpriteSwap:
if (!(graphic is Image)) height += EditorGUIUtility.singleLineHeight;
break;
case Selectable.Transition.Animation:
if (animator == null) height += EditorGUIUtility.singleLineHeight;
break;
}
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("colors"));
break;
case Selectable.Transition.SpriteSwap:
height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("spriteState"));
break;
case Selectable.Transition.Animation:
height += EditorGUI.GetPropertyHeight(transitionData.FindPropertyRelative("animationTriggers"));
break;
}
return height;
}
private void DrawTransitionData(Rect position, SerializedProperty transitionData, bool isChild = false)
{
SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic");
SerializedProperty transition = transitionData.FindPropertyRelative("transition");
SerializedProperty colorBlock = transitionData.FindPropertyRelative("colors");
SerializedProperty spriteState = transitionData.FindPropertyRelative("spriteState");
SerializedProperty animationTriggers = transitionData.FindPropertyRelative("animationTriggers");
EditorGUI.indentLevel++;
float lineHeight = EditorGUIUtility.singleLineHeight;
float spacing = 2f;
float y = position.y;
var currentTransition = GetTransition(transition);
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
case Selectable.Transition.SpriteSwap:
Rect targetRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.PropertyField(targetRect, targetGraphic);
y += lineHeight + spacing;
break;
}
Rect transitionRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.PropertyField(transitionRect, transition);
y += lineHeight + spacing;
var graphic = targetGraphic.objectReferenceValue as Graphic;
var animator = graphic != null ? graphic.GetComponent<Animator>() : null;
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
if (graphic == null)
{
Rect warningRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.HelpBox(warningRect, "需要Graphic组件来使用颜色过渡", MessageType.Warning);
y += lineHeight + spacing;
}
break;
case Selectable.Transition.SpriteSwap:
if (!(graphic is Image))
{
Rect warningRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.HelpBox(warningRect, "需要Image组件来使用精灵切换", MessageType.Warning);
y += lineHeight + spacing;
}
break;
case Selectable.Transition.Animation:
if (animator == null)
{
Rect warningRect = new Rect(position.x, y, position.width, lineHeight);
EditorGUI.HelpBox(warningRect, "需要Animator组件来使用动画切换", MessageType.Warning);
y += lineHeight + spacing;
}
break;
}
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
CheckAndSetColorDefaults(colorBlock, targetGraphic);
Rect colorRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(colorBlock));
EditorGUI.PropertyField(colorRect, colorBlock);
break;
case Selectable.Transition.SpriteSwap:
CheckAndSetColorDefaults(colorBlock, targetGraphic);
Rect spriteRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(spriteState));
EditorGUI.PropertyField(spriteRect, spriteState);
break;
case Selectable.Transition.Animation:
Rect animRect = new Rect(position.x, y, position.width, EditorGUI.GetPropertyHeight(animationTriggers));
EditorGUI.PropertyField(animRect, animationTriggers);
break;
}
if (graphic != null && currentTransition != (Selectable.Transition)transition.enumValueIndex &&
((Selectable.Transition)transition.enumValueIndex == Selectable.Transition.Animation ||
(Selectable.Transition)transition.enumValueIndex == Selectable.Transition.None))
{
graphic.canvasRenderer.SetColor(Color.white);
}
}
private void DrawSelfTransition()
{
GUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Main Transition", EditorStyles.boldLabel);
SerializedProperty targetGraphic = m_TransitionData.FindPropertyRelative("targetGraphic");
var graphic = targetGraphic.objectReferenceValue as Graphic;
if (graphic == null)
{
graphic = (target as UXButton).GetComponent<Graphic>();
targetGraphic.objectReferenceValue = graphic;
}
SerializedProperty transition = m_TransitionData.FindPropertyRelative("transition");
var currentTransition = GetTransition(transition);
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
case Selectable.Transition.SpriteSwap:
EditorGUILayout.PropertyField(targetGraphic);
break;
}
EditorGUILayout.PropertyField(transition);
var animator = graphic != null ? graphic.GetComponent<Animator>() : (target as UXButton).GetComponent<Animator>();
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
if (graphic == null)
EditorGUILayout.HelpBox("需要Graphic组件来使用颜色过渡", MessageType.Warning);
break;
case Selectable.Transition.SpriteSwap:
if (!(graphic is Image))
EditorGUILayout.HelpBox("需要Image组件来使用精灵切换", MessageType.Warning);
break;
case Selectable.Transition.Animation:
if (animator == null)
EditorGUILayout.HelpBox("需要Animator组件来使用动画切换", MessageType.Warning);
break;
}
switch (currentTransition)
{
case Selectable.Transition.ColorTint:
CheckAndSetColorDefaults(m_TransitionData.FindPropertyRelative("colors"), targetGraphic);
EditorGUILayout.PropertyField(m_TransitionData.FindPropertyRelative("colors"));
break;
case Selectable.Transition.SpriteSwap:
EditorGUILayout.PropertyField(m_TransitionData.FindPropertyRelative("spriteState"));
break;
case Selectable.Transition.Animation:
EditorGUILayout.PropertyField(m_TransitionData.FindPropertyRelative("animationTriggers"));
if (animator == null || animator.runtimeAnimatorController == null)
{
if (GUILayout.Button("Auto Generate Animation"))
{
var animationTrigger = m_TransitionData.FindPropertyRelative("animationTriggers");
var controller = GenerateSelectableAnimatorContoller(animationTrigger, target as UXButton);
if (controller != null)
{
if (animator == null)
animator = (target as UXButton).gameObject.AddComponent<Animator>();
UnityEditor.Animations.AnimatorController.SetAnimatorController(animator, controller);
}
}
}
break;
}
if (graphic != null && currentTransition != (Selectable.Transition)transition.enumValueIndex &&
((Selectable.Transition)transition.enumValueIndex == Selectable.Transition.Animation ||
(Selectable.Transition)transition.enumValueIndex == Selectable.Transition.None))
{
graphic.canvasRenderer.SetColor(Color.white);
}
EditorGUI.indentLevel--;
EditorGUILayout.Space();
GUILayout.EndVertical();
}
private static UnityEditor.Animations.AnimatorController GenerateSelectableAnimatorContoller(SerializedProperty property, UXButton target)
{
if (target == null)
return null;
var path = GetSaveControllerPath(target);
if (string.IsNullOrEmpty(path))
return null;
SerializedProperty normalTrigger = property.FindPropertyRelative("m_NormalTrigger");
SerializedProperty highlightedTrigger = property.FindPropertyRelative("m_HighlightedTrigger");
SerializedProperty pressedTrigger = property.FindPropertyRelative("m_PressedTrigger");
SerializedProperty selectedTrigger = property.FindPropertyRelative("m_SelectedTrigger");
SerializedProperty disabledTrigger = property.FindPropertyRelative("m_DisabledTrigger");
var normalName = string.IsNullOrEmpty(normalTrigger.stringValue) ? "Normal" : normalTrigger.stringValue;
var highlightedName = string.IsNullOrEmpty(highlightedTrigger.stringValue) ? "Highlighted" : highlightedTrigger.stringValue;
var pressedName = string.IsNullOrEmpty(pressedTrigger.stringValue) ? "Pressed" : pressedTrigger.stringValue;
var selectedName = string.IsNullOrEmpty(selectedTrigger.stringValue) ? "Selected" : selectedTrigger.stringValue;
var disabledName = string.IsNullOrEmpty(disabledTrigger.stringValue) ? "Disabled" : disabledTrigger.stringValue;
var controller = UnityEditor.Animations.AnimatorController.CreateAnimatorControllerAtPath(path);
GenerateTriggerableTransition(normalName, controller);
GenerateTriggerableTransition(highlightedName, controller);
GenerateTriggerableTransition(pressedName, controller);
GenerateTriggerableTransition(selectedName, controller);
GenerateTriggerableTransition(disabledName, controller);
AssetDatabase.ImportAsset(path);
return controller;
}
private static AnimationClip GenerateTriggerableTransition(string name, UnityEditor.Animations.AnimatorController controller)
{
// Create the clip
var clip = UnityEditor.Animations.AnimatorController.AllocateAnimatorClip(name);
AssetDatabase.AddObjectToAsset(clip, controller);
// Create a state in the animatior controller for this clip
var state = controller.AddMotion(clip);
// Add a transition property
controller.AddParameter(name, AnimatorControllerParameterType.Trigger);
// Add an any state transition
var stateMachine = controller.layers[0].stateMachine;
var transition = stateMachine.AddAnyStateTransition(state);
transition.AddCondition(UnityEditor.Animations.AnimatorConditionMode.If, 0, name);
return clip;
}
private static string GetSaveControllerPath(UXButton target)
{
var defaultName = target.gameObject.name;
var message = string.Format("Create a new animator for the game object '{0}':", defaultName);
return EditorUtility.SaveFilePanelInProject("New Animation Contoller", defaultName, "controller", message);
}
void CheckAndSetColorDefaults(SerializedProperty colorBlock, SerializedProperty targetGraphic)
{
bool isDirty = false;
string[] colorProps = new string[] { "m_NormalColor", "m_HighlightedColor", "m_PressedColor", "m_SelectedColor", "m_DisabledColor" };
foreach (var propName in colorProps)
{
SerializedProperty prop = colorBlock.FindPropertyRelative(propName);
Color color = prop.colorValue;
if (color.r == 0 && color.g == 0 && color.b == 0 && color.a == 0)
{
isDirty = true;
if (prop.name == "m_PressedColor")
{
prop.colorValue = new Color(0.7843137f, 0.7843137f, 0.7843137f, 1.0f);
}
else if (prop.name == "m_DisabledColor")
{
prop.colorValue = new Color(0.7843137f, 0.7843137f, 0.7843137f, 0.5f);
}
else
{
prop.colorValue = Color.white;
}
}
}
SerializedProperty fadeDuration = colorBlock.FindPropertyRelative("m_FadeDuration");
SerializedProperty m_ColorMultiplier = colorBlock.FindPropertyRelative("m_ColorMultiplier");
if (isDirty)
{
m_ColorMultiplier.floatValue = 1f;
fadeDuration.floatValue = 0.1f;
}
var graphic = targetGraphic.objectReferenceValue as Graphic;
if (graphic != null)
{
if (!EditorApplication.isPlaying && (Selectable.Transition)m_SelectionState.enumValueIndex != Selectable.Transition.Animation)
{
Color color = colorBlock.FindPropertyRelative("m_NormalColor").colorValue;
graphic.canvasRenderer.SetColor(color);
}
}
}
static Selectable.Transition GetTransition(SerializedProperty transition)
{
return (Selectable.Transition)transition.enumValueIndex;
}
}

View File

@ -1,50 +0,0 @@
using AlicizaX.Editor;
using UnityEditor;
namespace UnityEngine.UI
{
[CustomEditor(typeof(UXGroup))]
public class UXGroupInspector : GameFrameworkInspector
{
private SerializedProperty m_Buttons;
private UXGroup _target;
private void OnEnable()
{
_target = (UXGroup)target;
m_Buttons = serializedObject.FindProperty("m_Buttons");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
GUI.enabled = false;
DrawHotButtonListAlwaysExpanded(m_Buttons);
GUI.enabled = true;
EditorGUILayout.Space();
serializedObject.ApplyModifiedProperties();
}
private void DrawHotButtonListAlwaysExpanded(SerializedProperty listProp)
{
EditorGUILayout.LabelField("Hot Buttons", EditorStyles.boldLabel);
if (listProp.arraySize == 0)
{
EditorGUILayout.HelpBox("当前没有绑定任何 UXButton", MessageType.Info);
return;
}
for (int i = 0; i < listProp.arraySize; i++)
{
var element = listProp.GetArrayElementAtIndex(i);
var uxButton = element.objectReferenceValue as UXButton;
string name = uxButton != null ? uxButton.name : "Null";
EditorGUILayout.ObjectField($"[{i}] {name}", element.objectReferenceValue, typeof(UXButton), true);
}
}
}
}

View File

@ -15,35 +15,14 @@ public enum ButtonModeType
Toggle Toggle
} }
[Serializable] [ExecuteAlways]
public class TransitionData
{
public Graphic targetGraphic;
public Selectable.Transition transition = Selectable.Transition.ColorTint;
public ColorBlock colors = ColorBlock.defaultColorBlock;
public SpriteState spriteState;
public AnimationTriggers animationTriggers = new();
}
internal enum SelectionState
{
Normal,
Highlighted,
Pressed,
Selected,
Disabled,
}
[ExecuteInEditMode]
[DisallowMultipleComponent] [DisallowMultipleComponent]
public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler
{ {
#region Serialized Fields #region Serialized Fields
[SerializeField] private bool m_Interactable = true;
[SerializeField] private ButtonModeType m_Mode; [SerializeField] private ButtonModeType m_Mode;
[SerializeField] private Button.ButtonClickedEvent m_OnClick = new(); [SerializeField] private Button.ButtonClickedEvent m_OnClick = new();
[SerializeField] private TransitionData m_TransitionData = new();
[SerializeField] private List<TransitionData> m_ChildTransitions = new(); [SerializeField] private List<TransitionData> m_ChildTransitions = new();
[SerializeField] private UXGroup m_UXGroup; [SerializeField] private UXGroup m_UXGroup;
[SerializeField] private AudioClip hoverAudioClip; [SerializeField] private AudioClip hoverAudioClip;
@ -54,7 +33,6 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand
#region Private Fields #region Private Fields
[SerializeField] private SelectionState m_SelectionState = SelectionState.Normal;
private bool m_IsDown; private bool m_IsDown;
private bool m_HasExitedWhileDown; private bool m_HasExitedWhileDown;
private bool _mTogSelected; private bool _mTogSelected;
@ -84,17 +62,6 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand
private Animator Animator => _animator ? _animator : _animator = GetComponent<Animator>(); private Animator Animator => _animator ? _animator : _animator = GetComponent<Animator>();
public bool Interactable
{
get => m_Interactable;
set
{
if (m_Interactable == value) return;
m_Interactable = value;
SetState(m_Interactable ? SelectionState.Normal : SelectionState.Disabled);
}
}
public bool Selected public bool Selected
{ {
get => _mTogSelected; get => _mTogSelected;
@ -117,9 +84,7 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand
_mTogSelected = value; _mTogSelected = value;
m_OnValueChanged?.Invoke(value); m_OnValueChanged?.Invoke(value);
// ------------- 关键修复 -------------
// 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected无论逻辑是否为 selected // 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected无论逻辑是否为 selected
// 聚焦判定使用 m_IsNavFocusedOnSelect/OnDeselect 管理)或 EventSystem.current.currentSelectedGameObject == gameObject。
bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject; bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
bool isFocused = m_IsNavFocused || isEventSystemSelected; bool isFocused = m_IsNavFocused || isEventSystemSelected;
@ -155,7 +120,8 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand
protected override void Awake() protected override void Awake()
{ {
base.Awake(); base.Awake();
_waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration)); // 使用基类 m_MainTransition 的 fadeDuration
_waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_MainTransition.colors.fadeDuration));
ApplyVisualState(m_SelectionState, true); ApplyVisualState(m_SelectionState, true);
} }
@ -166,10 +132,6 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand
base.OnDestroy(); base.OnDestroy();
} }
protected override void OnSetProperty()
{
ApplyVisualState(m_SelectionState, true);
}
public override bool IsInteractable() public override bool IsInteractable()
{ {
@ -519,8 +481,8 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand
private void ForceSetState(SelectionState state) private void ForceSetState(SelectionState state)
{ {
m_SelectionState = state; // 链接到基类的 ForceSetState
ApplyVisualState(state, false); base.ForceSetState(state);
} }
#endregion #endregion
@ -554,74 +516,33 @@ public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHand
} }
} }
/// <summary>
/// 设置状态的入口(使用基类 SetState
/// </summary>
/// <param name="state"></param>
private void SetState(SelectionState state) private void SetState(SelectionState state)
{ {
if (m_SelectionState == state) return; ForceSetState(state);
m_SelectionState = state;
ApplyVisualState(state, false);
} }
private void ApplyVisualState(SelectionState state, bool instant) /// <summary>
/// 覆盖基类的 ApplyVisualState先由基类处理 MainTransition再处理 child transitions保持原逻辑
/// </summary>
public override void ApplyVisualState(SelectionState state, bool instant)
{ {
ApplyTransition(m_TransitionData, state, instant); // main transition 由基类处理
base.ApplyVisualState(state, instant);
// child transitions 保持原有处理UXButton 特有)
for (int i = 0; i < m_ChildTransitions.Count; i++) for (int i = 0; i < m_ChildTransitions.Count; i++)
ApplyTransition(m_ChildTransitions[i], state, instant); base.ApplyTransition(m_ChildTransitions[i], state, instant);
} }
private void ApplyTransition(TransitionData data, SelectionState state, bool instant) /// <summary>
{ /// 覆盖 PlayAnimation使用 UXButton 的 Animator 与 trigger 缓存(保持原逻辑)
if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation) /// </summary>
return; /// <param name="trigger"></param>
protected override void PlayAnimation(string trigger)
(Color color, Sprite sprite, string trigger) = state switch
{
SelectionState.Normal => (data.colors.normalColor, data.spriteState.highlightedSprite, data.animationTriggers.normalTrigger),
SelectionState.Highlighted => (data.colors.highlightedColor, data.spriteState.highlightedSprite, data.animationTriggers.highlightedTrigger),
SelectionState.Pressed => (data.colors.pressedColor, data.spriteState.pressedSprite, data.animationTriggers.pressedTrigger),
SelectionState.Selected => (data.colors.selectedColor, data.spriteState.selectedSprite, data.animationTriggers.selectedTrigger),
SelectionState.Disabled => (data.colors.disabledColor, data.spriteState.disabledSprite, data.animationTriggers.disabledTrigger),
_ => (Color.white, null, null)
};
switch (data.transition)
{
case Selectable.Transition.ColorTint:
TweenColor(data, color * data.colors.colorMultiplier, instant);
break;
case Selectable.Transition.SpriteSwap:
SwapSprite(data, sprite);
break;
case Selectable.Transition.Animation:
PlayAnimation(trigger);
break;
}
}
private void TweenColor(TransitionData data, Color color, bool instant)
{
if (data.targetGraphic == null) return;
if (Application.isPlaying)
{
data.targetGraphic.CrossFadeColor(
color,
instant ? 0f : data.colors.fadeDuration,
true,
true
);
}
else
{
data.targetGraphic.canvasRenderer.SetColor(color);
}
}
private static void SwapSprite(TransitionData data, Sprite sprite)
{
if (data.targetGraphic is Image img)
img.overrideSprite = sprite;
}
private void PlayAnimation(string trigger)
{ {
if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger)) if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger))
return; return;

View File

@ -22,7 +22,7 @@ namespace AlicizaX.UI.Extension.UXComponent.Hotkey
_holderObjectBase.OnWindowClosedEvent -= UnBindHotKeys; _holderObjectBase.OnWindowClosedEvent -= UnBindHotKeys;
} }
[SerializeField] [SerializeField]
private UXHotkey[] hotButtons; private UXHotkeyButton[] hotButtons;
internal void BindHotKeys() internal void BindHotKeys()
{ {

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 9aa1d0e145a6435c94190809bbc99235
timeCreated: 1760340422

View File

@ -1,29 +1,19 @@
#if INPUTSYSTEM_SUPPORT
using System;
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI.Extension namespace AlicizaX.UI.Extension
{ {
[DisallowMultipleComponent] [DisallowMultipleComponent]
public class UXHotkey : MonoBehaviour public class UXHotkeyButton : UXButton
{ {
[SerializeField] private UXButton _button;
[SerializeField] internal UnityEngine.InputSystem.InputActionReference _hotKeyRefrence; [SerializeField] internal UnityEngine.InputSystem.InputActionReference _hotKeyRefrence;
[SerializeField] internal EHotkeyPressType _hotkeyPressType; [SerializeField] internal EHotkeyPressType _hotkeyPressType;
private void OnValidate()
{
_button = GetComponent<UXButton>();
}
internal void HotkeyActionTrigger() internal void HotkeyActionTrigger()
{ {
if (_button.Interactable) if (Interactable)
{ {
_button.OnSubmit(null); OnSubmit(null);
} }
} }
} }
} }
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42ae64be990942c899bebfffed4b48a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 337b039db051cab44819dc51e6af1f43, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -4,6 +4,7 @@ using UnityEngine.InputSystem;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections; using System.Collections;
using AlicizaX.UI.Extension; using AlicizaX.UI.Extension;
internal enum EHotkeyPressType internal enum EHotkeyPressType
{ {
Started, Started,
@ -15,9 +16,9 @@ internal static class UXHotkeyRegisterManager
private readonly struct HotkeyRegistration private readonly struct HotkeyRegistration
{ {
public readonly EHotkeyPressType pressType; public readonly EHotkeyPressType pressType;
public readonly UXHotkey button; public readonly UXHotkeyButton button;
public HotkeyRegistration(UXHotkey btn, EHotkeyPressType pressType) public HotkeyRegistration(UXHotkeyButton btn, EHotkeyPressType pressType)
{ {
button = btn; button = btn;
this.pressType = pressType; this.pressType = pressType;
@ -32,8 +33,8 @@ internal static class UXHotkeyRegisterManager
new Dictionary<string, (System.Action<InputAction.CallbackContext>, InputActionReference)>(32); new Dictionary<string, (System.Action<InputAction.CallbackContext>, InputActionReference)>(32);
private static readonly Dictionary<UXHotkey, string> _buttonRegistrations = private static readonly Dictionary<UXHotkeyButton, string> _buttonRegistrations =
new Dictionary<UXHotkey, string>(64); new Dictionary<UXHotkeyButton, string>(64);
#if UNITY_EDITOR #if UNITY_EDITOR
@ -51,7 +52,7 @@ internal static class UXHotkeyRegisterManager
} }
#endif #endif
internal static void RegisterHotkey(UXHotkey button, InputActionReference action, EHotkeyPressType pressType) internal static void RegisterHotkey(UXHotkeyButton button, InputActionReference action, EHotkeyPressType pressType)
{ {
if (action == null || action.action == null || button == null) if (action == null || action.action == null || button == null)
return; return;
@ -92,7 +93,7 @@ internal static class UXHotkeyRegisterManager
} }
} }
public static void UnregisterHotkey(UXHotkey button) public static void UnregisterHotkey(UXHotkeyButton button)
{ {
if (button == null || !_buttonRegistrations.TryGetValue(button, out var actionId)) if (button == null || !_buttonRegistrations.TryGetValue(button, out var actionId))
return; return;
@ -150,7 +151,7 @@ internal static class UXHotkeyRegisterManager
public static class UXHotkeyHotkeyExtension public static class UXHotkeyHotkeyExtension
{ {
public static void BindHotKey(this UXHotkey button) public static void BindHotKey(this UXHotkeyButton button)
{ {
if (button == null) return; if (button == null) return;
@ -165,7 +166,7 @@ public static class UXHotkeyHotkeyExtension
} }
} }
public static void UnBindHotKey(this UXHotkey button) public static void UnBindHotKey(this UXHotkeyButton button)
{ {
if (button == null || button._hotKeyRefrence == null) return; if (button == null || button._hotKeyRefrence == null) return;
UXHotkeyRegisterManager.UnregisterHotkey(button); UXHotkeyRegisterManager.UnregisterHotkey(button);

View File

@ -0,0 +1,133 @@
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
internal static class MultipleDisplayUtilities
{
/// <summary>
/// Converts the current drag position into a relative position for the display.
/// </summary>
/// <param name="eventData"></param>
/// <param name="position"></param>
/// <returns>Returns true except when the drag operation is not on the same display as it originated</returns>
public static bool GetRelativeMousePositionForDrag(PointerEventData eventData, ref Vector2 position)
{
#if UNITY_EDITOR
position = eventData.position;
#else
int pressDisplayIndex = eventData.pointerPressRaycast.displayIndex;
var relativePosition = RelativeMouseAtScaled(eventData.position);
int currentDisplayIndex = (int)relativePosition.z;
// Discard events on a different display.
if (currentDisplayIndex != pressDisplayIndex)
return false;
// If we are not on the main display then we must use the relative position.
position = pressDisplayIndex != 0 ? (Vector2)relativePosition : eventData.position;
#endif
return true;
}
internal static Vector3 GetRelativeMousePositionForRaycast(PointerEventData eventData)
{
// The multiple display system is not supported on all platforms, when it is not supported the returned position
// will be all zeros so when the returned index is 0 we will default to the event data to be safe.
Vector3 eventPosition = RelativeMouseAtScaled(eventData.position);
if (eventPosition == Vector3.zero)
{
eventPosition = eventData.position;
#if UNITY_EDITOR
eventPosition.z = Display.activeEditorGameViewTarget;
#endif
// We don't really know in which display the event occurred. We will process the event assuming it occurred in our display.
}
// We support multiple display on some platforms. When supported:
// - InputSystem will set eventData.displayIndex
// - Old Input System will set eventPosition.z
//
// If the event is on the main display, both displayIndex and eventPosition.z
// will be 0 so in that case we can leave the eventPosition untouched (see UUM-47650).
#if ENABLE_INPUT_SYSTEM && PACKAGE_INPUTSYSTEM
if (eventData.displayIndex > 0)
eventPosition.z = eventData.displayIndex;
#endif
return eventPosition;
}
/// <summary>
/// A version of Display.RelativeMouseAt that scales the position when the main display has a different rendering resolution to the system resolution.
/// By default, the mouse position is relative to the main render area, we need to adjust this so it is relative to the system resolution
/// in order to correctly determine the position on other displays.
/// </summary>
/// <returns></returns>
public static Vector3 RelativeMouseAtScaled(Vector2 position)
{
#if !UNITY_EDITOR && !UNITY_WSA
// If the main display is now the same resolution as the system then we need to scale the mouse position. (case 1141732)
if (Display.main.renderingWidth != Display.main.systemWidth || Display.main.renderingHeight != Display.main.systemHeight)
{
// The system will add padding when in full-screen and using a non-native aspect ratio. (case UUM-7893)
// For example Rendering 1920x1080 with a systeem resolution of 3440x1440 would create black bars on each side that are 330 pixels wide.
// we need to account for this or it will offset our coordinates when we are not on the main display.
var systemAspectRatio = Display.main.systemWidth / (float)Display.main.systemHeight;
var sizePlusPadding = new Vector2(Display.main.renderingWidth, Display.main.renderingHeight);
var padding = Vector2.zero;
if (Screen.fullScreen)
{
var aspectRatio = Screen.width / (float)Screen.height;
if (Display.main.systemHeight * aspectRatio < Display.main.systemWidth)
{
// Horizontal padding
sizePlusPadding.x = Display.main.renderingHeight * systemAspectRatio;
padding.x = (sizePlusPadding.x - Display.main.renderingWidth) * 0.5f;
}
else
{
// Vertical padding
sizePlusPadding.y = Display.main.renderingWidth / systemAspectRatio;
padding.y = (sizePlusPadding.y - Display.main.renderingHeight) * 0.5f;
}
}
var sizePlusPositivePadding = sizePlusPadding - padding;
// If we are not inside of the main display then we must adjust the mouse position so it is scaled by
// the main display and adjusted for any padding that may have been added due to different aspect ratios.
if (position.y < -padding.y || position.y > sizePlusPositivePadding.y ||
position.x < -padding.x || position.x > sizePlusPositivePadding.x)
{
var adjustedPosition = position;
if (!Screen.fullScreen)
{
// When in windowed mode, the window will be centered with the 0,0 coordinate at the top left, we need to adjust so it is relative to the screen instead.
adjustedPosition.x -= (Display.main.renderingWidth - Display.main.systemWidth) * 0.5f;
adjustedPosition.y -= (Display.main.renderingHeight - Display.main.systemHeight) * 0.5f;
}
else
{
// Scale the mouse position to account for the black bars when in a non-native aspect ratio.
adjustedPosition += padding;
adjustedPosition.x *= Display.main.systemWidth / sizePlusPadding.x;
adjustedPosition.y *= Display.main.systemHeight / sizePlusPadding.y;
}
var relativePos = Display.RelativeMouseAt(adjustedPosition);
// If we are not on the main display then return the adjusted position.
if (relativePos.z != 0)
return relativePos;
}
// We are using the main display.
return new Vector3(position.x, position.y, 0);
}
#endif
return Display.RelativeMouseAt(position);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f5fbd4c3b1c84b51abda86322e214b05
timeCreated: 1765260757

View File

@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace AlicizaX.UI.Extension namespace UnityEngine.UI
{ {
internal static class SetPropertyUtility internal static class SetPropertyUtility
{ {

View File

@ -2,7 +2,7 @@ using System;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
namespace AlicizaX.UI.Extension namespace UnityEngine.UI
{ {
[Serializable] [Serializable]
public struct UXNavigation : IEquatable<UXNavigation> public struct UXNavigation : IEquatable<UXNavigation>

View File

@ -1,22 +1,50 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
namespace AlicizaX.UI.Extension namespace UnityEngine.UI
{ {
public class UXSelectable : [Serializable]
UIBehaviour, public class TransitionData
{
public Graphic targetGraphic;
public Selectable.Transition transition = Selectable.Transition.ColorTint;
public ColorBlock colors = ColorBlock.defaultColorBlock;
public SpriteState spriteState;
public AnimationTriggers animationTriggers = new AnimationTriggers();
}
[ExecuteAlways]
[SelectionBase]
[DisallowMultipleComponent]
public class UXSelectable : UIBehaviour,
IMoveHandler, IMoveHandler,
IPointerDownHandler, IPointerUpHandler, IPointerDownHandler, IPointerUpHandler,
IPointerEnterHandler, IPointerExitHandler, IPointerEnterHandler, IPointerExitHandler,
ISelectHandler, IDeselectHandler ISelectHandler, IDeselectHandler
{ {
protected static UXSelectable[] s_Selectables = new UXSelectable[10]; [Serializable]
protected static int s_SelectableCount = 0; public enum SelectionState
protected int m_CurrentIndex = -1; {
protected bool m_EnableCalled = false; Normal,
Highlighted,
Pressed,
Selected,
Disabled,
}
#region Fields
[SerializeField] protected UXNavigation m_Navigation = UXNavigation.defaultNavigation;
[SerializeField] protected bool m_Interactable = true;
[SerializeField] protected TransitionData m_MainTransition = new TransitionData();
// current visual / logical selection state (now in base)
[SerializeField] protected SelectionState m_SelectionState = SelectionState.Normal;
// runtime event flags (used by currentSelectionState)
protected bool isPointerInside { get; set; } protected bool isPointerInside { get; set; }
protected bool isPointerDown { get; set; } protected bool isPointerDown { get; set; }
protected bool hasSelection { get; set; } protected bool hasSelection { get; set; }
@ -24,14 +52,43 @@ namespace AlicizaX.UI.Extension
protected bool m_GroupsAllowInteraction = true; protected bool m_GroupsAllowInteraction = true;
private readonly List<CanvasGroup> m_CanvasGroupCache = new List<CanvasGroup>(); private readonly List<CanvasGroup> m_CanvasGroupCache = new List<CanvasGroup>();
[SerializeField] private UXNavigation m_Navigation = UXNavigation.defaultNavigation; // registry for navigation find functions
protected static UXSelectable[] s_Selectables = new UXSelectable[10];
protected static int s_SelectableCount = 0;
protected int m_CurrentIndex = -1;
protected bool m_EnableCalled = false;
public Graphic targetGraphic
{
get { return m_MainTransition.targetGraphic; }
set
{
if (SetPropertyUtility.SetClass(ref m_MainTransition.targetGraphic, value)) OnSetProperty();
}
}
#endregion
#region Properties
public UXNavigation navigation public UXNavigation navigation
{ {
get { return m_Navigation; } get { return m_Navigation; }
set set
{ {
if (SetPropertyUtility.SetStruct(ref m_Navigation, value)) OnSetProperty(); if (SetPropertyUtility.SetStruct(ref m_Navigation, value))
OnSetProperty();
}
}
public bool Interactable
{
get => m_Interactable;
set
{
if (m_Interactable == value) return;
m_Interactable = value;
OnSetProperty();
} }
} }
@ -45,10 +102,14 @@ namespace AlicizaX.UI.Extension
} }
} }
#endregion
protected virtual void OnSetProperty() #region Unity Lifecycle
protected override void Awake()
{ {
// override if needed base.Awake();
// nothing specific here; subclasses may initialize visuals
} }
protected override void OnEnable() protected override void OnEnable()
@ -77,6 +138,9 @@ namespace AlicizaX.UI.Extension
m_GroupsAllowInteraction = ParentGroupAllowsInteraction(); m_GroupsAllowInteraction = ParentGroupAllowsInteraction();
m_EnableCalled = true; m_EnableCalled = true;
// Ensure visual state matches current settings immediately
OnSetProperty();
} }
protected override void OnDisable() protected override void OnDisable()
@ -90,6 +154,8 @@ namespace AlicizaX.UI.Extension
s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount]; s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount];
s_Selectables[s_SelectableCount] = null; s_Selectables[s_SelectableCount] = null;
InstantClearState();
base.OnDisable(); base.OnDisable();
m_EnableCalled = false; m_EnableCalled = false;
} }
@ -104,6 +170,10 @@ namespace AlicizaX.UI.Extension
} }
} }
#endregion
#region Groups & Interactable
protected bool ParentGroupAllowsInteraction() protected bool ParentGroupAllowsInteraction()
{ {
Transform t = transform; Transform t = transform;
@ -126,9 +196,176 @@ namespace AlicizaX.UI.Extension
public virtual bool IsInteractable() public virtual bool IsInteractable()
{ {
return m_GroupsAllowInteraction; return m_GroupsAllowInteraction && m_Interactable;
} }
#endregion
#region Property change handling & SelectionState API
/// <summary>
/// Called when a property that affects visuals changes (navigation/interactable/animation/etc).
/// </summary>
protected virtual void OnSetProperty()
{
// If not interactable => Disabled state
if (!IsInteractable())
{
m_SelectionState = SelectionState.Disabled;
DoStateTransition(SelectionState.Disabled, false);
}
else
{
// If previously disabled, restore to Normal
if (m_SelectionState == SelectionState.Disabled)
m_SelectionState = SelectionState.Normal;
// Apply current selection state
DoStateTransition(m_SelectionState, false);
}
}
/// <summary>
/// Computed selection state based on pointer / selection flags and interactability.
/// Mirrors Unity's Selectable.currentSelectionState behavior.
/// </summary>
protected SelectionState currentSelectionState
{
get
{
if (!IsInteractable())
return SelectionState.Disabled;
if (isPointerDown)
return SelectionState.Pressed;
if (hasSelection)
return SelectionState.Selected;
if (isPointerInside)
return SelectionState.Highlighted;
return SelectionState.Normal;
}
}
/// <summary>
/// Evaluate and transition to the computed selection state. Call after changing pointer/selection flags.
/// </summary>
protected void EvaluateAndTransitionToSelectionState()
{
if (!IsActive() || !IsInteractable())
return;
DoStateTransition(currentSelectionState, false);
}
/// <summary>
/// Force apply visual state immediately.
/// </summary>
protected void ForceSetState(SelectionState state)
{
m_SelectionState = state;
DoStateTransition(state, true);
}
/// <summary>
/// Clear internal state and restore visuals (used on disable / focus lost).
/// </summary>
protected virtual void InstantClearState()
{
// reset flags
isPointerInside = false;
isPointerDown = false;
hasSelection = false;
// restore visuals based on main transition's normal state
if (m_MainTransition == null) return;
switch (m_MainTransition.transition)
{
case Selectable.Transition.ColorTint:
TweenColor(m_MainTransition, m_MainTransition.colors.normalColor * m_MainTransition.colors.colorMultiplier, true);
break;
case Selectable.Transition.SpriteSwap:
SwapSprite(m_MainTransition, null);
break;
case Selectable.Transition.Animation:
PlayAnimation(m_MainTransition.animationTriggers.normalTrigger);
break;
}
m_SelectionState = SelectionState.Normal;
}
/// <summary>
/// Core mapping from SelectionState -> TransitionData and execution.
/// Subclasses can override PlayAnimation or ApplyVisualState for custom behavior.
/// </summary>
/// <param name="state"></param>
/// <param name="instant"></param>
protected virtual void DoStateTransition(SelectionState state, bool instant)
{
if (!gameObject.activeInHierarchy)
return;
if (m_MainTransition == null)
return;
Color tintColor = Color.white;
Sprite sprite = null;
string trigger = null;
switch (state)
{
case SelectionState.Normal:
tintColor = m_MainTransition.colors.normalColor;
sprite = m_MainTransition.spriteState.highlightedSprite;
trigger = m_MainTransition.animationTriggers.normalTrigger;
break;
case SelectionState.Highlighted:
tintColor = m_MainTransition.colors.highlightedColor;
sprite = m_MainTransition.spriteState.highlightedSprite;
trigger = m_MainTransition.animationTriggers.highlightedTrigger;
break;
case SelectionState.Pressed:
tintColor = m_MainTransition.colors.pressedColor;
sprite = m_MainTransition.spriteState.pressedSprite;
trigger = m_MainTransition.animationTriggers.pressedTrigger;
break;
case SelectionState.Selected:
tintColor = m_MainTransition.colors.selectedColor;
sprite = m_MainTransition.spriteState.selectedSprite;
trigger = m_MainTransition.animationTriggers.selectedTrigger;
break;
case SelectionState.Disabled:
tintColor = m_MainTransition.colors.disabledColor;
sprite = m_MainTransition.spriteState.disabledSprite;
trigger = m_MainTransition.animationTriggers.disabledTrigger;
break;
}
// Execute appropriate transition type
switch (m_MainTransition.transition)
{
case Selectable.Transition.ColorTint:
TweenColor(m_MainTransition, tintColor * m_MainTransition.colors.colorMultiplier, instant);
break;
case Selectable.Transition.SpriteSwap:
SwapSprite(m_MainTransition, sprite);
break;
case Selectable.Transition.Animation:
PlayAnimation(trigger);
break;
case Selectable.Transition.None:
default:
break;
}
// keep serialized state in sync
m_SelectionState = state;
}
#endregion
#region Navigation helpers
public UXSelectable FindSelectable(Vector3 dir) public UXSelectable FindSelectable(Vector3 dir)
{ {
dir = dir.normalized; dir = dir.normalized;
@ -201,12 +438,6 @@ namespace AlicizaX.UI.Extension
return dir; return dir;
} }
void Navigate(AxisEventData eventData, UXSelectable sel)
{
if (sel != null && sel.IsActive())
eventData.selectedObject = sel.gameObject;
}
public virtual UXSelectable FindSelectableOnLeft() public virtual UXSelectable FindSelectableOnLeft()
{ {
if (m_Navigation.mode == UXNavigation.Mode.Explicit) if (m_Navigation.mode == UXNavigation.Mode.Explicit)
@ -262,6 +493,16 @@ namespace AlicizaX.UI.Extension
} }
} }
void Navigate(AxisEventData eventData, UXSelectable sel)
{
if (sel != null && sel.IsActive())
eventData.selectedObject = sel.gameObject;
}
#endregion
#region Pointer / Select base implementations (update flags + evaluate)
public virtual void OnPointerDown(PointerEventData eventData) public virtual void OnPointerDown(PointerEventData eventData)
{ {
if (eventData.button != PointerEventData.InputButton.Left) if (eventData.button != PointerEventData.InputButton.Left)
@ -269,28 +510,143 @@ namespace AlicizaX.UI.Extension
if (IsInteractable() && navigation.mode != UXNavigation.Mode.None && EventSystem.current != null) if (IsInteractable() && navigation.mode != UXNavigation.Mode.None && EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(gameObject, eventData); EventSystem.current.SetSelectedGameObject(gameObject, eventData);
isPointerDown = true;
EvaluateAndTransitionToSelectionState();
} }
public virtual void OnPointerUp(PointerEventData eventData) public virtual void OnPointerUp(PointerEventData eventData)
{ {
if (eventData.button != PointerEventData.InputButton.Left)
return;
isPointerDown = false;
EvaluateAndTransitionToSelectionState();
} }
public virtual void OnPointerEnter(PointerEventData eventData) public virtual void OnPointerEnter(PointerEventData eventData)
{ {
isPointerInside = true;
EvaluateAndTransitionToSelectionState();
} }
public virtual void OnPointerExit(PointerEventData eventData) public virtual void OnPointerExit(PointerEventData eventData)
{ {
isPointerInside = false;
EvaluateAndTransitionToSelectionState();
} }
public virtual void OnSelect(BaseEventData eventData) public virtual void OnSelect(BaseEventData eventData)
{ {
hasSelection = true; hasSelection = true;
EvaluateAndTransitionToSelectionState();
} }
public virtual void OnDeselect(BaseEventData eventData) public virtual void OnDeselect(BaseEventData eventData)
{ {
hasSelection = false; hasSelection = false;
EvaluateAndTransitionToSelectionState();
}
#endregion
#region Visual transition helpers (main transition + low level ops)
/// <summary>
/// High-level API: apply main transition visuals (child classes can override, UXButton overrides to add child transitions).
/// </summary>
/// <param name="state"></param>
/// <param name="instant"></param>
public virtual void ApplyVisualState(SelectionState state, bool instant)
{
if (m_MainTransition != null)
ApplyTransition(m_MainTransition, state, instant);
}
/// <summary>
/// Apply a single TransitionData (handles ColorTint / SpriteSwap / Animation).
/// Animation triggering calls PlayAnimation (virtual).
/// </summary>
protected void ApplyTransition(TransitionData data, SelectionState state, bool instant)
{
if (data == null) return;
if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation)
return;
Color color = Color.white;
Sprite sprite = null;
string trigger = null;
switch (state)
{
case SelectionState.Normal:
color = data.colors.normalColor;
sprite = data.spriteState.highlightedSprite;
trigger = data.animationTriggers.normalTrigger;
break;
case SelectionState.Highlighted:
color = data.colors.highlightedColor;
sprite = data.spriteState.highlightedSprite;
trigger = data.animationTriggers.highlightedTrigger;
break;
case SelectionState.Pressed:
color = data.colors.pressedColor;
sprite = data.spriteState.pressedSprite;
trigger = data.animationTriggers.pressedTrigger;
break;
case SelectionState.Selected:
color = data.colors.selectedColor;
sprite = data.spriteState.selectedSprite;
trigger = data.animationTriggers.selectedTrigger;
break;
case SelectionState.Disabled:
color = data.colors.disabledColor;
sprite = data.spriteState.disabledSprite;
trigger = data.animationTriggers.disabledTrigger;
break;
}
switch (data.transition)
{
case Selectable.Transition.ColorTint:
TweenColor(data, color * data.colors.colorMultiplier, instant);
break;
case Selectable.Transition.SpriteSwap:
SwapSprite(data, sprite);
break;
case Selectable.Transition.Animation:
PlayAnimation(trigger);
break;
} }
} }
protected void TweenColor(TransitionData data, Color color, bool instant)
{
if (data == null || data.targetGraphic == null) return;
data.targetGraphic.CrossFadeColor(
color,
instant ? 0f : data.colors.fadeDuration,
true,
true
);
}
protected static void SwapSprite(TransitionData data, Sprite sprite)
{
if (data == null) return;
if (data.targetGraphic is Image img)
img.overrideSprite = sprite;
}
/// <summary>
/// Subclasses override this to trigger Animator triggers, etc.
/// </summary>
/// <param name="trigger"></param>
protected virtual void PlayAnimation(string trigger)
{
// base does nothing — subclasses (e.g. UXButton) can override to use Animator.
}
#endregion
}
} }

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 0e3d476361ae4f1ea2e5143663661f3c guid: 0e3d476361ae4f1ea2e5143663661f3c
timeCreated: 1764667719 MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 1ab39039c9d7d844aa962517519f0ad6, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b940f76ec6e9b0a45a47464a6f3d9616
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,603 @@
using AlicizaX.UI.Extension;
using UnityEngine.EventSystems;
using Direction = UnityEngine.UI.Slider.Direction;
namespace UnityEngine.UI
{
[ExecuteAlways]
[RequireComponent(typeof(RectTransform))]
public class UXSlider : UXSelectable, IDragHandler, IInitializePotentialDragHandler, ICanvasElement
{
[SerializeField] private RectTransform m_FillRect;
public RectTransform fillRect
{
get { return m_FillRect; }
set
{
if (SetPropertyUtility.SetClass(ref m_FillRect, value))
{
UpdateCachedReferences();
UpdateVisuals();
}
}
}
[SerializeField] private RectTransform m_HandleRect;
public RectTransform handleRect
{
get { return m_HandleRect; }
set
{
if (SetPropertyUtility.SetClass(ref m_HandleRect, value))
{
UpdateCachedReferences();
UpdateVisuals();
}
}
}
[Space] [SerializeField] private Slider.Direction m_Direction = Slider.Direction.LeftToRight;
public Slider.Direction direction
{
get { return m_Direction; }
set
{
if (SetPropertyUtility.SetStruct(ref m_Direction, value)) UpdateVisuals();
}
}
[SerializeField] private float m_MinValue = 0;
public float minValue
{
get { return m_MinValue; }
set
{
if (SetPropertyUtility.SetStruct(ref m_MinValue, value))
{
Set(m_Value);
UpdateVisuals();
}
}
}
[SerializeField] private float m_MaxValue = 1;
public float maxValue
{
get { return m_MaxValue; }
set
{
if (SetPropertyUtility.SetStruct(ref m_MaxValue, value))
{
Set(m_Value);
UpdateVisuals();
}
}
}
[SerializeField] private bool m_WholeNumbers = false;
public bool wholeNumbers
{
get { return m_WholeNumbers; }
set
{
if (SetPropertyUtility.SetStruct(ref m_WholeNumbers, value))
{
Set(m_Value);
UpdateVisuals();
}
}
}
[Space] [SerializeField] protected float m_Value;
public virtual float value
{
get { return wholeNumbers ? Mathf.Round(m_Value) : m_Value; }
set { Set(value); }
}
public virtual void SetValueWithoutNotify(float input)
{
Set(input, false);
}
public float normalizedValue
{
get
{
if (Mathf.Approximately(minValue, maxValue))
return 0;
return Mathf.InverseLerp(minValue, maxValue, value);
}
set { this.value = Mathf.Lerp(minValue, maxValue, value); }
}
[Space] [SerializeField] private Slider.SliderEvent m_OnValueChanged = new Slider.SliderEvent();
public Slider.SliderEvent onValueChanged
{
get { return m_OnValueChanged; }
set { m_OnValueChanged = value; }
}
// --- SMOOTHING FIELDS ---
[Space]
[SerializeField] private bool m_SmoothMovement = false;
[SerializeField] private float m_SmoothSpeed = 8f; // value-per-second, larger=faster
public bool smoothMovement
{
get { return m_SmoothMovement; }
set { m_SmoothMovement = value; }
}
/// <summary>
/// 平滑移动目标值(只有当 smoothMovement && !wholeNumbers 时生效)
/// </summary>
protected float m_TargetValue;
protected bool m_IsSmoothMoving = false;
// ------------------------
private Image m_FillImage;
private Transform m_FillTransform;
private RectTransform m_FillContainerRect;
private Transform m_HandleTransform;
private RectTransform m_HandleContainerRect;
private Vector2 m_Offset = Vector2.zero;
#pragma warning disable 649
private DrivenRectTransformTracker m_Tracker;
#pragma warning restore 649
private bool m_DelayedUpdateVisuals = false;
float stepSize
{
get { return wholeNumbers ? 1 : (maxValue - minValue) * 0.1f; }
}
protected UXSlider()
{
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
if (wholeNumbers)
{
m_MinValue = Mathf.Round(m_MinValue);
m_MaxValue = Mathf.Round(m_MaxValue);
}
//Onvalidate is called before OnEnabled. We need to make sure not to touch any other objects before OnEnable is run.
if (IsActive())
{
UpdateCachedReferences();
// Update rects in next update since other things might affect them even if value didn't change.
m_DelayedUpdateVisuals = true;
}
if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying)
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
}
#endif
protected override void OnEnable()
{
base.OnEnable();
UpdateCachedReferences();
Set(m_Value, false);
// Update rects since they need to be initialized correctly.
UpdateVisuals();
// 初始化 target
m_TargetValue = m_Value;
m_IsSmoothMoving = false;
}
protected override void OnDisable()
{
m_Tracker.Clear();
base.OnDisable();
}
protected virtual void Update()
{
if (m_DelayedUpdateVisuals)
{
m_DelayedUpdateVisuals = false;
Set(m_Value, false);
UpdateVisuals();
}
// 处理平滑移动(仅当开启时)
if (m_IsSmoothMoving)
{
// 如果 wholeNumbers 为 true则取消平滑直接设置最终值避免小数
if (wholeNumbers)
{
m_IsSmoothMoving = false;
Set(m_TargetValue);
}
else
{
float maxDelta = m_SmoothSpeed * Time.unscaledDeltaTime;
float newValue = Mathf.MoveTowards(m_Value, m_TargetValue, maxDelta);
// 使用 Set 来保持更新视觉并触发回调(会在数值改变时触发)
Set(newValue);
if (Mathf.Approximately(newValue, m_TargetValue))
{
m_IsSmoothMoving = false;
// 确保最终值精确到目标(避免浮点残留)
Set(m_TargetValue);
}
}
}
}
protected override void OnDidApplyAnimationProperties()
{
m_Value = ClampValue(m_Value);
float oldNormalizedValue = normalizedValue;
if (m_FillContainerRect != null)
{
if (m_FillImage != null && m_FillImage.type == Image.Type.Filled)
oldNormalizedValue = m_FillImage.fillAmount;
else
oldNormalizedValue = (reverseValue ? 1 - m_FillRect.anchorMin[(int)axis] : m_FillRect.anchorMax[(int)axis]);
}
else if (m_HandleContainerRect != null)
oldNormalizedValue = (reverseValue ? 1 - m_HandleRect.anchorMin[(int)axis] : m_HandleRect.anchorMin[(int)axis]);
UpdateVisuals();
if (oldNormalizedValue != normalizedValue)
{
UISystemProfilerApi.AddMarker("Slider.value", this);
onValueChanged.Invoke(m_Value);
}
// UUM-34170 Apparently, some properties on slider such as IsInteractable and Normalcolor Animation is broken.
// We need to call base here to render the animation on Scene
base.OnDidApplyAnimationProperties();
}
void UpdateCachedReferences()
{
if (m_FillRect && m_FillRect != (RectTransform)transform)
{
m_FillTransform = m_FillRect.transform;
m_FillImage = m_FillRect.GetComponent<Image>();
if (m_FillTransform.parent != null)
m_FillContainerRect = m_FillTransform.parent.GetComponent<RectTransform>();
}
else
{
m_FillRect = null;
m_FillContainerRect = null;
m_FillImage = null;
}
if (m_HandleRect && m_HandleRect != (RectTransform)transform)
{
m_HandleTransform = m_HandleRect.transform;
if (m_HandleTransform.parent != null)
m_HandleContainerRect = m_HandleTransform.parent.GetComponent<RectTransform>();
}
else
{
m_HandleRect = null;
m_HandleContainerRect = null;
}
}
float ClampValue(float input)
{
float newValue = Mathf.Clamp(input, minValue, maxValue);
if (wholeNumbers)
newValue = Mathf.Round(newValue);
return newValue;
}
protected virtual void Set(float input, bool sendCallback = true)
{
// Clamp the input
float newValue = ClampValue(input);
// If the stepped value doesn't match the last one, it's time to update
if (m_Value == newValue)
return;
m_Value = newValue;
UpdateVisuals();
if (sendCallback)
{
UISystemProfilerApi.AddMarker("Slider.value", this);
m_OnValueChanged.Invoke(newValue);
}
}
protected override void OnRectTransformDimensionsChange()
{
base.OnRectTransformDimensionsChange();
if (!IsActive())
return;
UpdateVisuals();
}
enum Axis
{
Horizontal = 0,
Vertical = 1
}
Axis axis
{
get { return (m_Direction == Direction.LeftToRight || m_Direction == Direction.RightToLeft) ? Axis.Horizontal : Axis.Vertical; }
}
bool reverseValue
{
get { return m_Direction == Direction.RightToLeft || m_Direction == Direction.TopToBottom; }
}
private void UpdateVisuals()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
UpdateCachedReferences();
#endif
m_Tracker.Clear();
if (m_FillContainerRect != null)
{
m_Tracker.Add(this, m_FillRect, DrivenTransformProperties.Anchors);
Vector2 anchorMin = Vector2.zero;
Vector2 anchorMax = Vector2.one;
if (m_FillImage != null && m_FillImage.type == Image.Type.Filled)
{
m_FillImage.fillAmount = normalizedValue;
}
else
{
if (reverseValue)
anchorMin[(int)axis] = 1 - normalizedValue;
else
anchorMax[(int)axis] = normalizedValue;
}
m_FillRect.anchorMin = anchorMin;
m_FillRect.anchorMax = anchorMax;
}
if (m_HandleContainerRect != null)
{
m_Tracker.Add(this, m_HandleRect, DrivenTransformProperties.Anchors);
Vector2 anchorMin = Vector2.zero;
Vector2 anchorMax = Vector2.one;
anchorMin[(int)axis] = anchorMax[(int)axis] = (reverseValue ? (1 - normalizedValue) : normalizedValue);
m_HandleRect.anchorMin = anchorMin;
m_HandleRect.anchorMax = anchorMax;
}
}
void UpdateDrag(PointerEventData eventData, Camera cam)
{
RectTransform clickRect = m_HandleContainerRect ?? m_FillContainerRect;
if (clickRect != null && clickRect.rect.size[(int)axis] > 0)
{
Vector2 position = Vector2.zero;
if (!MultipleDisplayUtilities.GetRelativeMousePositionForDrag(eventData, ref position))
return;
Vector2 localCursor;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(clickRect, position, cam, out localCursor))
return;
localCursor -= clickRect.rect.position;
float val = Mathf.Clamp01((localCursor - m_Offset)[(int)axis] / clickRect.rect.size[(int)axis]);
normalizedValue = (reverseValue ? 1f - val : val);
}
}
private bool MayDrag(PointerEventData eventData)
{
return IsActive() && IsInteractable() && eventData.button == PointerEventData.InputButton.Left;
}
public override void OnPointerDown(PointerEventData eventData)
{
if (!MayDrag(eventData))
return;
base.OnPointerDown(eventData);
// 取消任何平滑移动(开始拖拽时)
m_IsSmoothMoving = false;
m_TargetValue = m_Value;
m_Offset = Vector2.zero;
if (m_HandleContainerRect != null && RectTransformUtility.RectangleContainsScreenPoint(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.enterEventCamera))
{
Vector2 localMousePos;
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(m_HandleRect, eventData.pointerPressRaycast.screenPosition, eventData.pressEventCamera, out localMousePos))
m_Offset = localMousePos;
}
else
{
// Outside the slider handle - jump to this point instead
UpdateDrag(eventData, eventData.pressEventCamera);
}
}
public void OnDrag(PointerEventData eventData)
{
if (!MayDrag(eventData))
return;
// 拖拽也取消平滑
m_IsSmoothMoving = false;
m_TargetValue = m_Value;
UpdateDrag(eventData, eventData.pressEventCamera);
}
public override void OnMove(AxisEventData eventData)
{
if (!IsActive() || !IsInteractable())
{
base.OnMove(eventData);
return;
}
// 基准值:如果当前已经在平滑移动中,累加到 target 上;否则从当前 value 开始。
float currentBase = m_IsSmoothMoving ? m_TargetValue : value;
switch (eventData.moveDir)
{
case MoveDirection.Left:
if (axis == Axis.Horizontal && FindSelectableOnLeft() == null)
{
float dest = ClampValue(reverseValue ? currentBase + stepSize : currentBase - stepSize);
if (m_SmoothMovement && !wholeNumbers)
{
m_TargetValue = dest;
m_IsSmoothMoving = true;
}
else
{
Set(dest);
}
}
else
base.OnMove(eventData);
break;
case MoveDirection.Right:
if (axis == Axis.Horizontal && FindSelectableOnRight() == null)
{
float dest = ClampValue(reverseValue ? currentBase - stepSize : currentBase + stepSize);
if (m_SmoothMovement && !wholeNumbers)
{
m_TargetValue = dest;
m_IsSmoothMoving = true;
}
else
{
Set(dest);
}
}
else
base.OnMove(eventData);
break;
case MoveDirection.Up:
if (axis == Axis.Vertical && FindSelectableOnUp() == null)
{
float dest = ClampValue(reverseValue ? currentBase - stepSize : currentBase + stepSize);
if (m_SmoothMovement && !wholeNumbers)
{
m_TargetValue = dest;
m_IsSmoothMoving = true;
}
else
{
Set(dest);
}
}
else
base.OnMove(eventData);
break;
case MoveDirection.Down:
if (axis == Axis.Vertical && FindSelectableOnDown() == null)
{
float dest = ClampValue(reverseValue ? currentBase + stepSize : currentBase - stepSize);
if (m_SmoothMovement && !wholeNumbers)
{
m_TargetValue = dest;
m_IsSmoothMoving = true;
}
else
{
Set(dest);
}
}
else
base.OnMove(eventData);
break;
}
}
public override UXSelectable FindSelectableOnLeft()
{
if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Horizontal)
return null;
return base.FindSelectableOnLeft();
}
public override UXSelectable FindSelectableOnRight()
{
if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Horizontal)
return null;
return base.FindSelectableOnRight();
}
public override UXSelectable FindSelectableOnUp()
{
if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Vertical)
return null;
return base.FindSelectableOnUp();
}
public override UXSelectable FindSelectableOnDown()
{
if (navigation.mode == UXNavigation.Mode.Automatic && axis == Axis.Vertical)
return null;
return base.FindSelectableOnDown();
}
public void OnInitializePotentialDrag(PointerEventData eventData)
{
eventData.useDragThreshold = false;
}
public void SetDirection(Direction direction, bool includeRectLayouts)
{
Axis oldAxis = axis;
bool oldReverse = reverseValue;
this.direction = direction;
if (!includeRectLayouts)
return;
if (axis != oldAxis)
RectTransformUtility.FlipLayoutAxes(transform as RectTransform, true, true);
if (reverseValue != oldReverse)
RectTransformUtility.FlipLayoutOnAxis(transform as RectTransform, (int)axis, true, true);
}
public void Rebuild(CanvasUpdate executing)
{
#if UNITY_EDITOR
if (executing == CanvasUpdate.Prelayout)
onValueChanged.Invoke(value);
#endif
}
public void LayoutComplete()
{
}
public void GraphicUpdateComplete()
{
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 000a654558f849878601d3b0ed350623
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: dfe8ade815753dc4d9ca3ce5d981cb91, type: 3}
userData:
assetBundleName:
assetBundleVariant: