重构
1.重构UXButton 2.主Transition有UXSeletable负责 ,同时兼具导航 3.新增UXSlider 支持平滑过渡 适配手柄 4.优化部分逻辑bug
This commit is contained in:
parent
132426b9b8
commit
32c0fc13fd
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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")]
|
||||||
|
|||||||
BIN
Editor/Res/ComponentIcon/Selectable Icon.png
Normal file
BIN
Editor/Res/ComponentIcon/Selectable Icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 437 B |
127
Editor/Res/ComponentIcon/Selectable Icon.png.meta
Normal file
127
Editor/Res/ComponentIcon/Selectable Icon.png.meta
Normal 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:
|
||||||
BIN
Editor/Res/ComponentIcon/Slider Icon.png
Normal file
BIN
Editor/Res/ComponentIcon/Slider Icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 457 B |
127
Editor/Res/ComponentIcon/Slider Icon.png.meta
Normal file
127
Editor/Res/ComponentIcon/Slider Icon.png.meta
Normal 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
3
Editor/UX/Button.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: baf1c03f83bf4c46aaf15b1cb5ff48cd
|
||||||
|
timeCreated: 1765184499
|
||||||
402
Editor/UX/Button/UXButtonEditor.cs
Normal file
402
Editor/UX/Button/UXButtonEditor.cs
Normal 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
3
Editor/UX/Group.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 21c6f15c77fa4c6abbe65350c76028a4
|
||||||
|
timeCreated: 1765184506
|
||||||
153
Editor/UX/Group/UXGroupInspector.cs
Normal file
153
Editor/UX/Group/UXGroupInspector.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 87a10dc3a1544ec586ff27809213a127
|
|
||||||
timeCreated: 1760341800
|
|
||||||
31
Editor/UX/Hotkey/UXHotkeyButtonEditor.cs
Normal file
31
Editor/UX/Hotkey/UXHotkeyButtonEditor.cs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta
Normal file
3
Editor/UX/Hotkey/UXHotkeyButtonEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dc0b010e4f884f6992de90c5e5dd7154
|
||||||
|
timeCreated: 1765185454
|
||||||
3
Editor/UX/Iamge.meta
Normal file
3
Editor/UX/Iamge.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 681126dbfd104770895c5b2dedb80aa0
|
||||||
|
timeCreated: 1765184491
|
||||||
3
Editor/UX/Selectable.meta
Normal file
3
Editor/UX/Selectable.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 29e9949ea8274c14b76e93d316a2d101
|
||||||
|
timeCreated: 1765184529
|
||||||
570
Editor/UX/Selectable/UXSelectableEditor.cs
Normal file
570
Editor/UX/Selectable/UXSelectableEditor.cs
Normal 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>
|
||||||
|
/// 子类调用:绘制基类的通用 Inspector(Navigation / 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
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Editor/UX/Selectable/UXSelectableEditor.cs.meta
Normal file
3
Editor/UX/Selectable/UXSelectableEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 1e63c9ae25da41d482430905b232d091
|
||||||
|
timeCreated: 1765183957
|
||||||
3
Editor/UX/Slider.meta
Normal file
3
Editor/UX/Slider.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 708a130e1d5f49f18619c6ad9c3baccb
|
||||||
|
timeCreated: 1765260889
|
||||||
158
Editor/UX/Slider/UXSliderEditor.cs
Normal file
158
Editor/UX/Slider/UXSliderEditor.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Editor/UX/Slider/UXSliderEditor.cs.meta
Normal file
3
Editor/UX/Slider/UXSliderEditor.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e67f2a4d5c0a4f399f67374dcfd42a1f
|
||||||
|
timeCreated: 1765260896
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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_IsNavFocused(OnSelect/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;
|
||||||
|
|||||||
@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 9aa1d0e145a6435c94190809bbc99235
|
|
||||||
timeCreated: 1760340422
|
|
||||||
@ -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
|
|
||||||
11
Runtime/UXComponent/Hotkey/UXHotkeyButton.cs.meta
Normal file
11
Runtime/UXComponent/Hotkey/UXHotkeyButton.cs.meta
Normal 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:
|
||||||
@ -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);
|
||||||
|
|||||||
133
Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs
Normal file
133
Runtime/UXComponent/Selectable/MultipleDisplayUtilities.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f5fbd4c3b1c84b51abda86322e214b05
|
||||||
|
timeCreated: 1765260757
|
||||||
@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
8
Runtime/UXComponent/UXSlider.meta
Normal file
8
Runtime/UXComponent/UXSlider.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b940f76ec6e9b0a45a47464a6f3d9616
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
603
Runtime/UXComponent/UXSlider/UXSlider.cs
Normal file
603
Runtime/UXComponent/UXSlider/UXSlider.cs
Normal 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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Runtime/UXComponent/UXSlider/UXSlider.cs.meta
Normal file
11
Runtime/UXComponent/UXSlider/UXSlider.cs.meta
Normal 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:
|
||||||
Loading…
Reference in New Issue
Block a user