From 35a5416cc470eaf54318fc318e981469ce40e012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Fri, 11 Apr 2025 17:26:28 +0800 Subject: [PATCH] modify --- Editor/Res.meta | 8 + Editor/Res/d_Button Icon.png | Bin 0 -> 782 bytes Editor/Res/d_Button Icon.png.meta | 117 +++++++ Editor/Res/d_ToggleGroup Icon.png | Bin 0 -> 487 bytes Editor/Res/d_ToggleGroup Icon.png.meta | 117 +++++++ Editor/UX.meta | 3 + Editor/UX/UXButtonEditor.cs | 309 +++++++++++++++++ Editor/UX/UXButtonEditor.cs.meta | 3 + Editor/UX/UXGroupEditor.cs | 29 ++ Editor/UX/UXGroupEditor.cs.meta | 3 + Runtime/RecyclerView/ViewHolder/ViewHolder.cs | 5 + Runtime/UGUIExtension/UX.meta | 3 + Runtime/UGUIExtension/UX/UXButton.cs | 312 ++++++++++++++++++ Runtime/UGUIExtension/UX/UXButton.cs.meta | 11 + Runtime/UGUIExtension/UX/UXGroup.cs | 124 +++++++ Runtime/UGUIExtension/UX/UXGroup.cs.meta | 11 + 16 files changed, 1055 insertions(+) create mode 100644 Editor/Res.meta create mode 100644 Editor/Res/d_Button Icon.png create mode 100644 Editor/Res/d_Button Icon.png.meta create mode 100644 Editor/Res/d_ToggleGroup Icon.png create mode 100644 Editor/Res/d_ToggleGroup Icon.png.meta create mode 100644 Editor/UX.meta create mode 100644 Editor/UX/UXButtonEditor.cs create mode 100644 Editor/UX/UXButtonEditor.cs.meta create mode 100644 Editor/UX/UXGroupEditor.cs create mode 100644 Editor/UX/UXGroupEditor.cs.meta create mode 100644 Runtime/UGUIExtension/UX.meta create mode 100644 Runtime/UGUIExtension/UX/UXButton.cs create mode 100644 Runtime/UGUIExtension/UX/UXButton.cs.meta create mode 100644 Runtime/UGUIExtension/UX/UXGroup.cs create mode 100644 Runtime/UGUIExtension/UX/UXGroup.cs.meta diff --git a/Editor/Res.meta b/Editor/Res.meta new file mode 100644 index 0000000..2bdbb38 --- /dev/null +++ b/Editor/Res.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1c397bc7b14cfb8439960f6f74c953ca +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Res/d_Button Icon.png b/Editor/Res/d_Button Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..19862c6788c71da0c4a6dbf19cf7800f721ee652 GIT binary patch literal 782 zcmV+p1M&QcP)UxCCj3()O$?@uO^ z=gM2+JyPB<{O>BylgZ?X@^P_P{9*d%77H0@x7+s;iNyYNI^C9HYL2Qa?2)kba=H9- z4%V4#K$CAt@RJ1BRbEN3rLvVurM|AMt?}i~Xf)bzUH7pt6*Vg=)52V;d?6DjVB*q^ z20EQiO~elpN~&y1bgxt@eTx&LUz~@+zgM*?{6ys|aT<(8rC0;z%UdEzZ~8`Ga)3au z*V`PA$1hcVs{DXHr3>ZLOeXWOP$-4f$~oDdt1$(#Pc8wM#OoVed=LX4c z|J~$qEiF&m7K<|Ag;zhw2<#s9oK5J#AYr%D_1QV!HvJ+Jq(R9@o7| z+F;=@js{w2Mm??7Lj)gTyysR$?_zGRws39;Lns%75`JxApJpw{r!40qtR`HJCZTB>(^b M07*qoM6N<$g2n}Jx&QzG literal 0 HcmV?d00001 diff --git a/Editor/Res/d_Button Icon.png.meta b/Editor/Res/d_Button Icon.png.meta new file mode 100644 index 0000000..10a5f5c --- /dev/null +++ b/Editor/Res/d_Button Icon.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 337b039db051cab44819dc51e6af1f43 +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: 1 + 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: 4 + 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: 4 + 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 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Res/d_ToggleGroup Icon.png b/Editor/Res/d_ToggleGroup Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..858eab14f1f74877396f8c6fe63e125e8a557f9a GIT binary patch literal 487 zcmVWeEAjMuFEm}e5AzQL+54QPK zyGVR&&%F1TH+*)x+ji|0h+G9|n&!Fh`_Ew*_O95e^(czYaU6dnN%F16#p=Lzy#_gO zaRb28Ih+EP-nmL>!*U8x0k*g{1i>atFd_mcBzc;qUp}d;3b2vab^T64a!!oV16&}$X^r)iBPUCb zod4hoslZ8kN@Rr)H0qs89x7lY!vX$E5lhHJ1?0ldQHUaK zj1@q>N{n$RIdVdJ03VbHcc)eYAlT-RBQHn*5^N&ok^|4%0tCxV+CkrpH_XwPebvew z?mSC?AjP}=g#-4TXDw*k_8uVgn4NvWTyZumRzTNvPXNSklr7Okrmq77SjN0QQI_Qe zb4Z~5ksh@_J!Z`@TLEgUJ?%f#;Rk8Qn#2;z_({Ou5Xfb~k~6AE-;akW45I>Ddp;%R dwsxHr_y^C1?d7s$N$mgt002ovPDHLkV1g@w*-8Ka literal 0 HcmV?d00001 diff --git a/Editor/Res/d_ToggleGroup Icon.png.meta b/Editor/Res/d_ToggleGroup Icon.png.meta new file mode 100644 index 0000000..1750e5e --- /dev/null +++ b/Editor/Res/d_ToggleGroup Icon.png.meta @@ -0,0 +1,117 @@ +fileFormatVersion: 2 +guid: 42b2d97a2cb439b4395c6dca63357d89 +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: 1 + 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: 4 + 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: 4 + 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 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + customData: + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spriteCustomMetadata: + entries: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/UX.meta b/Editor/UX.meta new file mode 100644 index 0000000..5730f82 --- /dev/null +++ b/Editor/UX.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f770d49c712145179c9d1e9d6cd6b141 +timeCreated: 1744275046 \ No newline at end of file diff --git a/Editor/UX/UXButtonEditor.cs b/Editor/UX/UXButtonEditor.cs new file mode 100644 index 0000000..46c519c --- /dev/null +++ b/Editor/UX/UXButtonEditor.cs @@ -0,0 +1,309 @@ +// UXButtonEditor.cs + +#if UNITY_EDITOR +using System; +using System.Collections.Generic; +using Sirenix.OdinInspector.Editor; +using UnityEditor; +using UnityEditor.AnimatedValues; +using UnityEditor.UI; +using UnityEngine; +using UnityEngine.UI; + +[CanEditMultipleObjects] +[CustomEditor(typeof(UXButton), true)] +public class UXButtonEditor : Editor +{ + 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_ButtonUISounds; + + private bool m_ShowChildTransitions = true; + private Dictionary m_ChildTransitionFoldouts = new Dictionary(); + private UXGroup group; + private int m_ButtonMode; + + private void OnEnable() + { + 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_ButtonUISounds = serializedObject.FindProperty("m_ButtonUISounds"); + m_ChildTransitionFoldouts.Clear(); + group = (UXGroup)m_UXGroup.objectReferenceValue; + m_ButtonMode = m_Mode.enumValueIndex; + } + + private void ResetEventProperty(SerializedProperty property) + { + SerializedProperty persistentCalls = property.FindPropertyRelative("m_PersistentCalls"); + SerializedProperty calls = persistentCalls.FindPropertyRelative("m_Calls"); + calls.arraySize = 0; + property.serializedObject.ApplyModifiedProperties(); + } + public override void OnInspectorGUI() + { + serializedObject.Update(); + + EditorGUI.BeginDisabledGroup(EditorApplication.isPlaying); + EditorGUILayout.PropertyField(m_Mode); + EditorGUI.EndDisabledGroup(); + EditorGUILayout.PropertyField(m_Interactable); + + GUILayout.Space(1); + DrawSelfTransition(); + + GUILayout.Space(5); + DrawChildTransitions(); + + + EditorGUILayout.PropertyField(m_ButtonUISounds); + EditorGUILayout.Space(); + + if (m_Mode.enumValueIndex != m_ButtonMode) + { + if (m_ButtonMode == (int)ButtonModeType.Normal) + { + ResetEventProperty(m_OnValueChanged); + m_UXGroup.objectReferenceValue = null; + } + else + { + ResetEventProperty((m_OnClick)); + } + + m_ButtonMode = m_Mode.enumValueIndex; + } + + if (m_Mode.enumValueIndex == (int)ButtonModeType.Toggle) + { + EditorGUILayout.PropertyField(m_UXGroup); + UXGroup newGroup = (UXGroup)m_UXGroup.objectReferenceValue; + if (newGroup != group) + { + UXButton self = target as UXButton; + if (group != null) + { + group.UnregisterButton(self); + } + + group = newGroup; + if (newGroup != null) + { + newGroup.RegisterButton(self); + } + } + + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(m_OnValueChanged); + } + else + { + EditorGUILayout.Space(); + EditorGUILayout.PropertyField(m_OnClick); + } + + + serializedObject.ApplyModifiedProperties(); + } + + private void DrawChildTransitions() + { + m_ShowChildTransitions = EditorGUILayout.Foldout(m_ShowChildTransitions, "Child Transitions", true); + if (!m_ShowChildTransitions) + return; + + EditorGUI.indentLevel++; + + // 列表控制按钮 + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Add Child Transition", GUILayout.Width(150))) + { + m_ChildTransitions.arraySize++; + serializedObject.ApplyModifiedProperties(); + } + + EditorGUILayout.EndHorizontal(); + + // 遍历列表元素 + for (int i = 0; i < m_ChildTransitions.arraySize; i++) + { + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // 获取当前元素的SerializedProperty + SerializedProperty element = m_ChildTransitions.GetArrayElementAtIndex(i); + + // 初始化折叠状态 + if (!m_ChildTransitionFoldouts.ContainsKey(i)) + m_ChildTransitionFoldouts[i] = true; + + // 折叠标题 + string elementTitle = $"Child Transition {i}"; + if (element.FindPropertyRelative("targetGraphic").objectReferenceValue != null) + elementTitle += $" ({element.FindPropertyRelative("targetGraphic").objectReferenceValue.name})"; + + m_ChildTransitionFoldouts[i] = EditorGUILayout.Foldout(m_ChildTransitionFoldouts[i], elementTitle, true); + + if (m_ChildTransitionFoldouts[i]) + { + // 绘制单个TransitionData + DrawTransitionData(element); + + // 删除按钮 + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + var orginColor = GUI.backgroundColor; + GUI.backgroundColor = Color.red; + if (GUILayout.Button("Remove", GUILayout.Width(80))) + { + m_ChildTransitions.DeleteArrayElementAtIndex(i); + m_ChildTransitionFoldouts.Remove(i); + serializedObject.ApplyModifiedProperties(); + break; // 删除后退出当前循环 + } + + GUI.backgroundColor = orginColor; + + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(5); + } + + EditorGUI.indentLevel--; + } + + private void DrawTransitionData(SerializedProperty transitionData) + { + SerializedProperty targetGraphic = transitionData.FindPropertyRelative("targetGraphic"); + SerializedProperty transition = transitionData.FindPropertyRelative("transition"); + SerializedProperty colorBlock = transitionData.FindPropertyRelative("colors"); + SerializedProperty spriteState = transitionData.FindPropertyRelative("spriteState"); + + EditorGUI.indentLevel++; + + // 绘制目标图形 + EditorGUILayout.PropertyField(targetGraphic); + + // 获取当前transition类型 + var currentTransition = GetTransition(transition); + + // 绘制transition类型 + EditorGUILayout.PropertyField(transition); + + // 显示警告信息 + var graphic = targetGraphic.objectReferenceValue as Graphic; + 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; + } + + // 绘制对应类型的属性 + switch (currentTransition) + { + case Selectable.Transition.ColorTint: + CheckAndSetColorDefaults(colorBlock, targetGraphic); + EditorGUILayout.PropertyField(colorBlock); + break; + + case Selectable.Transition.SpriteSwap: + EditorGUILayout.PropertyField(spriteState); + break; + } + + EditorGUI.indentLevel--; + } + + 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) + { + Color color = colorBlock.FindPropertyRelative("m_NormalColor").colorValue; + graphic.canvasRenderer.SetColor(color); + } + } + } + + private void DrawSelfTransition() + { + 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(); + targetGraphic.objectReferenceValue = graphic; + } + + EditorGUI.indentLevel++; + DrawTransitionData(m_TransitionData); + EditorGUI.indentLevel--; + EditorGUILayout.Space(); + } + + + static Selectable.Transition GetTransition(SerializedProperty transition) + { + return (Selectable.Transition)transition.enumValueIndex; + } +} + +#endif diff --git a/Editor/UX/UXButtonEditor.cs.meta b/Editor/UX/UXButtonEditor.cs.meta new file mode 100644 index 0000000..a2c0be7 --- /dev/null +++ b/Editor/UX/UXButtonEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1a6fb65845fb481293f57b30b1bfcb3b +timeCreated: 1744275051 \ No newline at end of file diff --git a/Editor/UX/UXGroupEditor.cs b/Editor/UX/UXGroupEditor.cs new file mode 100644 index 0000000..b523426 --- /dev/null +++ b/Editor/UX/UXGroupEditor.cs @@ -0,0 +1,29 @@ +// using UnityEditor; +// +// #if UNITY_EDITOR +// [CustomEditor(typeof(UXGroup))] +// public class UXGroupEditor : Editor +// { +// private SerializedProperty m_AllowSwitchOff; +// private SerializedProperty m_Buttons; +// private SerializedProperty m_OnSelectedChanged; +// +// private void OnEnable() +// { +// m_AllowSwitchOff = serializedObject.FindProperty("m_AllowSwitchOff"); +// m_Buttons = serializedObject.FindProperty("m_Buttons"); +// m_OnSelectedChanged = serializedObject.FindProperty("onSelectedChanged"); +// } +// +// public override void OnInspectorGUI() +// { +// serializedObject.Update(); +// +// EditorGUILayout.PropertyField(m_AllowSwitchOff); +// EditorGUILayout.PropertyField(m_Buttons, true); +// EditorGUILayout.PropertyField(m_OnSelectedChanged); +// +// serializedObject.ApplyModifiedProperties(); +// } +// } +// #endif diff --git a/Editor/UX/UXGroupEditor.cs.meta b/Editor/UX/UXGroupEditor.cs.meta new file mode 100644 index 0000000..3bbd213 --- /dev/null +++ b/Editor/UX/UXGroupEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d66e1f78170d455c93d71e71ee8f735a +timeCreated: 1744275087 \ No newline at end of file diff --git a/Runtime/RecyclerView/ViewHolder/ViewHolder.cs b/Runtime/RecyclerView/ViewHolder/ViewHolder.cs index 48e7378..dcfb9ee 100644 --- a/Runtime/RecyclerView/ViewHolder/ViewHolder.cs +++ b/Runtime/RecyclerView/ViewHolder/ViewHolder.cs @@ -42,6 +42,11 @@ namespace AlicizaX.UI.RecyclerView button.onClick.RemoveAllListeners(); button.onClick.AddListener(() => action?.Invoke(data)); } + else if (TryGetComponent(out UXButton uxButton)) + { + uxButton.onClick.RemoveAllListeners(); + uxButton.onClick.AddListener(() => action?.Invoke(data)); + } } protected internal void BindChoiceState(bool state) diff --git a/Runtime/UGUIExtension/UX.meta b/Runtime/UGUIExtension/UX.meta new file mode 100644 index 0000000..e8bdfc6 --- /dev/null +++ b/Runtime/UGUIExtension/UX.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d002cbb3729643c7813fdfc55b8beb79 +timeCreated: 1744274178 \ No newline at end of file diff --git a/Runtime/UGUIExtension/UX/UXButton.cs b/Runtime/UGUIExtension/UX/UXButton.cs new file mode 100644 index 0000000..3f7b11f --- /dev/null +++ b/Runtime/UGUIExtension/UX/UXButton.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using AlicizaX; +using AlicizaX.UI.Extension; +using UnityEngine; +using UnityEngine.Events; +using UnityEngine.EventSystems; +using UnityEngine.Serialization; +using UnityEngine.UI; +using AudioType = AlicizaX.Audio.Runtime.AudioType; + +[Serializable] +public enum ButtonModeType +{ + Normal, + Toggle +} + +[System.Serializable] +public class TransitionData +{ + public Graphic targetGraphic; + public Selectable.Transition transition = Selectable.Transition.ColorTint; + public ColorBlock colors; + public SpriteState spriteState; +} + +[Serializable] +public struct ButtonSoundData +{ + public ButtonSoundType ButtonSoundType; + public string ButtonUISoundName; +} + + +internal enum SelectionState +{ + /// + /// The UI object can be selected. + /// + Normal, + + /// + /// The UI object is highlighted. + /// + Highlighted, + + /// + /// The UI object is pressed. + /// + Pressed, + + /// + /// The UI object is selected + /// + Selected, + + /// + /// The UI object cannot be selected. + /// + Disabled, +} + +[DisallowMultipleComponent] +[RequireComponent(typeof(Graphic))] +public class UXButton : UIBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler +{ + [SerializeField] private bool m_Interactable = true; + + + [SerializeField] private ButtonModeType m_Mode; + + [SerializeField] private UnityEvent m_OnClick = new UnityEvent(); + + [SerializeField] private TransitionData m_TransitionData = new TransitionData(); + + [SerializeField] private List m_ChildTransitions = new List(); + [SerializeField] private UXGroup m_UXGroup; + + [SerializeField] private List m_ButtonUISounds = new List(); + + private SelectionState m_SelectionState = SelectionState.Normal; + private bool m_ExistUI; + private bool m_IsDown; + private bool m_IsTogSelected; + + + public bool IsSelected + { + get { return m_IsTogSelected; } + internal set + { + m_IsTogSelected = value; + onValueChanged?.Invoke(m_IsTogSelected); + m_SelectionState = m_IsTogSelected ? SelectionState.Selected : SelectionState.Normal; + UpdateVisualState(m_SelectionState, false); + } + } + + + public UnityEvent onClick + { + get { return m_OnClick; } + set { m_OnClick = value; } + } + + [SerializeField] private UnityEvent m_OnValueChanged = new UnityEvent(); + + public UnityEvent onValueChanged + { + get { return m_OnValueChanged; } + set { m_OnValueChanged = value; } + } + + protected override void Awake() + { + base.Awake(); + if (m_Mode == ButtonModeType.Toggle) + { + onValueChanged?.Invoke(IsSelected); + } + } + + + void IPointerDownHandler.OnPointerDown(PointerEventData eventData) + { + if (!m_Interactable) return; + m_IsDown = true; + m_SelectionState = SelectionState.Pressed; + UpdateVisualState(m_SelectionState, false); + PlayButtonSound(ButtonSoundType.Down); + } + + void IPointerUpHandler.OnPointerUp(PointerEventData eventData) + { + if (!m_Interactable) return; + m_IsDown = false; + + if (!m_IsTogSelected) + { + m_SelectionState = m_ExistUI ? SelectionState.Normal : SelectionState.Highlighted; + UpdateVisualState(m_SelectionState, false); + } + else + { + m_SelectionState = SelectionState.Selected; + UpdateVisualState(m_SelectionState, false); + } + + if (!m_ExistUI) + { + ProcessClick(); + } + + PlayButtonSound(ButtonSoundType.Up); + } + + void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData) + { + if (!m_Interactable || CantTouch()) return; + m_SelectionState = SelectionState.Highlighted; + m_ExistUI = false; + UpdateVisualState(m_SelectionState, false); + PlayButtonSound(ButtonSoundType.Enter); + } + + void IPointerExitHandler.OnPointerExit(PointerEventData eventData) + { + if (!m_Interactable) return; + if (m_IsDown) + { + m_ExistUI = true; + return; + } + + if (CantTouch()) + { + return; + } + + m_SelectionState = SelectionState.Normal; + + UpdateVisualState(m_SelectionState, false); + PlayButtonSound(ButtonSoundType.Exit); + } + + private bool CantTouch() + { + return m_Mode == ButtonModeType.Toggle && m_IsTogSelected; + } + + private void ProcessClick() + { + if (m_Mode == ButtonModeType.Normal) + { + onClick?.Invoke(); + } + else + { + if (m_UXGroup) + { + m_UXGroup.NotifyButtonClicked(this); + return; + } + + IsSelected = !IsSelected; + } + } + + private void UpdateVisualState(SelectionState state, bool instant) + { + ProcessTransitionData(m_TransitionData, state, instant); + foreach (var transition in m_ChildTransitions) + { + ProcessTransitionData(transition, state, instant); + } + } + + + private void ProcessTransitionData(TransitionData transition, SelectionState state, bool instant) + { + if (transition.targetGraphic == null) return; + + Color tintColor; + Sprite transitionSprite; + switch (state) + { + case SelectionState.Normal: + tintColor = transition.colors.normalColor; + transitionSprite = null; + break; + case SelectionState.Highlighted: + tintColor = transition.colors.highlightedColor; + transitionSprite = transition.spriteState.highlightedSprite; + break; + case SelectionState.Pressed: + tintColor = transition.colors.pressedColor; + transitionSprite = transition.spriteState.pressedSprite; + break; + case SelectionState.Selected: + tintColor = transition.colors.selectedColor; + transitionSprite = transition.spriteState.selectedSprite; + break; + case SelectionState.Disabled: + tintColor = transition.colors.disabledColor; + transitionSprite = transition.spriteState.disabledSprite; + break; + default: + tintColor = Color.black; + transitionSprite = null; + break; + } + + switch (transition.transition) + { + case Selectable.Transition.ColorTint: + StartColorTween(transition, tintColor * transition.colors.colorMultiplier, instant); + break; + case Selectable.Transition.SpriteSwap: + DoSpriteSwap(transition, transitionSprite); + break; + } + } + + protected void StartColorTween(TransitionData transitionData, Color targetColor, bool instant) + { + if (Application.isPlaying) + { + transitionData.targetGraphic.CrossFadeColor(targetColor, instant ? 0f : transitionData.colors.fadeDuration, true, true); + } + else + { + transitionData.targetGraphic.canvasRenderer.SetColor(targetColor); + } + } + + protected void DoSpriteSwap(TransitionData transitionData, Sprite newSprite) + { + if (transitionData.targetGraphic is Image image) + { + image.overrideSprite = newSprite; + } + else if (transitionData.targetGraphic != null) + { + Log.Error($"Target Graphic must be Image for SpriteSwap. Object: {transitionData.targetGraphic.name}"); + } + } + + protected void PlayButtonSound(ButtonSoundType buttonSoundType) + { + ButtonSoundCell buttonSound = GetButtonSound(buttonSoundType); + if (buttonSound == null || string.IsNullOrEmpty(buttonSound.ButtonUISoundName)) + { + return; + } + + GameApp.Audio.Play(AudioType.UISound, buttonSound.ButtonUISoundName, false, GameApp.Audio.UISoundVolume, true); + } + + protected ButtonSoundCell GetButtonSound(ButtonSoundType buttonSoundType) + { + foreach (var buttonSound in m_ButtonUISounds) + { + if (buttonSound.ButtonSoundType == buttonSoundType) + { + return buttonSound; + } + } + + return null; + } +} diff --git a/Runtime/UGUIExtension/UX/UXButton.cs.meta b/Runtime/UGUIExtension/UX/UXButton.cs.meta new file mode 100644 index 0000000..c29d0d4 --- /dev/null +++ b/Runtime/UGUIExtension/UX/UXButton.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d7e92b092d584bb39e5239463f064cbe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 337b039db051cab44819dc51e6af1f43, type: 3} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/UGUIExtension/UX/UXGroup.cs b/Runtime/UGUIExtension/UX/UXGroup.cs new file mode 100644 index 0000000..286cdbb --- /dev/null +++ b/Runtime/UGUIExtension/UX/UXGroup.cs @@ -0,0 +1,124 @@ +// UXGroup.cs + +using UnityEngine; +using UnityEngine.Events; +using System.Collections.Generic; +using Sirenix.OdinInspector; +using UnityEngine.EventSystems; + +[DisallowMultipleComponent] +public class UXGroup : UIBehaviour +{ + [SerializeField] private bool m_AllowSwitchOff; + [ReadOnly] [SerializeField] private List m_Buttons = new List(); + + private UXButton currentUXButton = null; + + public UnityEvent onSelectedChanged = new UnityEvent(); + + public bool allowSwitchOff + { + get => m_AllowSwitchOff; + set + { + m_AllowSwitchOff = value; + ValidateGroupState(); + } + } + + protected override void OnDestroy() + { + base.OnDestroy(); + m_Buttons.Clear(); + currentUXButton = null; + } + + + protected override void Awake() + { + base.Awake(); + ValidateGroupState(); + } + + public void RegisterButton(UXButton button) + { + if (!m_Buttons.Contains(button)) + { + m_Buttons.Add(button); + if (button.IsSelected) + { + if (currentUXButton != null && currentUXButton != button) + { + currentUXButton.IsSelected = false; + } + currentUXButton = button; + } + ValidateGroupState(); + } + } + + public void UnregisterButton(UXButton button) + { + if (m_Buttons.Contains(button)) + { + m_Buttons.Remove(button); + button.IsSelected = false; + } + } + + internal void NotifyButtonClicked(UXButton clickedButton) + { + if (!clickedButton.IsSelected) + { + SetSelectedButton(clickedButton); + } + else + { + if (m_AllowSwitchOff) + SetSelectedButton(null); + else if (currentUXButton != clickedButton) + clickedButton.IsSelected = true; + } + } + + private void SetSelectedButton(UXButton targetButton) + { + UXButton previousSelected = currentUXButton; + currentUXButton = null; // 防止递归 + + foreach (var button in m_Buttons) + { + bool shouldSelect = (button == targetButton); + if (button.IsSelected != shouldSelect) + { + button.IsSelected = shouldSelect; + } + if (shouldSelect) currentUXButton = button; + } + + if (previousSelected != currentUXButton) + { + onSelectedChanged.Invoke(currentUXButton); + } + } + + private void ValidateGroupState() + { + bool anySelected = m_Buttons.Exists(b => b.IsSelected); + if (!anySelected && m_Buttons.Count > 0 && !m_AllowSwitchOff) + { + SetSelectedButton(m_Buttons[0]); + } + } + + public bool AnyOtherSelected(UXButton exclusion) + { + foreach (var button in m_Buttons) + { + if (button != exclusion && button.IsSelected) + return true; + } + + return false; + } +} diff --git a/Runtime/UGUIExtension/UX/UXGroup.cs.meta b/Runtime/UGUIExtension/UX/UXGroup.cs.meta new file mode 100644 index 0000000..fb0d956 --- /dev/null +++ b/Runtime/UGUIExtension/UX/UXGroup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f0492ee9ffe496c9f028b5f28a10308 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 42b2d97a2cb439b4395c6dca63357d89, type: 3} + userData: + assetBundleName: + assetBundleVariant: