UXButton UXGroup优化

This commit is contained in:
陈思海 2025-07-28 13:04:49 +08:00
parent 06aed57569
commit 39630c4f78
12 changed files with 339 additions and 185 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 864 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

View File

@ -0,0 +1,117 @@
fileFormatVersion: 2
guid: c4398d454b1a861499ef73d23bc7a032
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: 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:

View File

@ -211,7 +211,7 @@ MonoBehaviour:
m_ImagePosition: 0 m_ImagePosition: 0
m_ContentOffset: {x: 15, y: 1} m_ContentOffset: {x: 15, y: 1}
m_FixedWidth: 24 m_FixedWidth: 24
m_FixedHeight: 15 m_FixedHeight: 24
m_StretchWidth: 1 m_StretchWidth: 1
m_StretchHeight: 0 m_StretchHeight: 0
m_label: m_label:

View File

@ -9,7 +9,7 @@ using UnityEngine.UI;
[CanEditMultipleObjects] [CanEditMultipleObjects]
[CustomEditor(typeof(UXButton), true)] [CustomEditor(typeof(UXButton), true)]
public class UXButtonEditor : Editor internal class UXButtonEditor : Editor
{ {
private enum TabType private enum TabType
{ {

View File

@ -1,29 +0,0 @@
// 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

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: d66e1f78170d455c93d71e71ee8f735a
timeCreated: 1744275087

View File

@ -5,7 +5,7 @@ MonoImporter:
serializedVersion: 2 serializedVersion: 2
defaultReferences: [] defaultReferences: []
executionOrder: 0 executionOrder: 0
icon: {instanceID: 0} icon: {fileID: 2800000, guid: c4398d454b1a861499ef73d23bc7a032, type: 3}
userData: userData:
assetBundleName: assetBundleName:
assetBundleVariant: assetBundleVariant:

View File

@ -1,11 +1,11 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using AlicizaX; using AlicizaX;
using AlicizaX.UI.Extension; using AlicizaX.UI.Extension;
using UnityEngine; using UnityEngine;
using UnityEngine.Events; using UnityEngine.Events;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.UI; using UnityEngine.UI;
using AudioType = AlicizaX.Audio.Runtime.AudioType; using AudioType = AlicizaX.Audio.Runtime.AudioType;
@ -16,7 +16,6 @@ public enum ButtonModeType
Toggle Toggle
} }
[System.Serializable] [System.Serializable]
public class TransitionData public class TransitionData
{ {
@ -24,102 +23,130 @@ public class TransitionData
public Selectable.Transition transition = Selectable.Transition.ColorTint; public Selectable.Transition transition = Selectable.Transition.ColorTint;
public ColorBlock colors; public ColorBlock colors;
public SpriteState spriteState; public SpriteState spriteState;
public AnimationTriggers animationTriggers = new AnimationTriggers(); public AnimationTriggers animationTriggers = new();
} }
internal enum SelectionState internal enum SelectionState
{ {
Normal, Normal,
Highlighted, Highlighted,
Pressed, Pressed,
Selected, Selected,
Disabled, Disabled,
} }
[ExecuteInEditMode]
[DisallowMultipleComponent] [DisallowMultipleComponent]
public class UXButton : UIBehaviour, IButton, public class UXButton : UIBehaviour, IButton,
IPointerDownHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler,
IPointerUpHandler, IPointerExitHandler, IPointerClickHandler, ISubmitHandler
IPointerEnterHandler,
IPointerExitHandler,
IPointerClickHandler
{ {
#region Serialized Fields
[SerializeField] private bool m_Interactable = true; [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 Button.ButtonClickedEvent(); [SerializeField] private TransitionData m_TransitionData = new();
[SerializeField] private List<TransitionData> m_ChildTransitions = new();
[SerializeField] private TransitionData m_TransitionData = new TransitionData();
[SerializeField] private List<TransitionData> m_ChildTransitions = new List<TransitionData>();
[SerializeField] private UXGroup m_UXGroup; [SerializeField] private UXGroup m_UXGroup;
[SerializeField] private AudioClip hoverAudioClip; [SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip; [SerializeField] private AudioClip clickAudioClip;
#endregion
#region Private Variables
[SerializeField] private SelectionState m_SelectionState = SelectionState.Normal; [SerializeField] private SelectionState m_SelectionState = SelectionState.Normal;
private bool m_DownAndExistUI; private bool m_DownAndExistUI;
private bool m_IsDown; private bool m_IsDown;
private bool m_IsTogSelected; private bool m_IsTogSelected;
private Animator _animator; private Animator _animator;
private WaitForSeconds _waitTimeFadeDuration;
private Coroutine _resetRoutine;
private bool _boardEvent;
private readonly Dictionary<string, int> _animTriggerIDs = new();
private readonly Dictionary<string, int> _animResetTriggerIDs = new();
#endregion
#region Properties
internal Animator animator internal Animator animator
{ {
get get
{ {
_animator = _animator ?? GetComponent<Animator>(); if (!_animator)
_animator = GetComponent<Animator>();
return _animator; return _animator;
} }
} }
public bool IsSelected public bool IsSelected
{ {
get { return m_IsTogSelected; } get => m_IsTogSelected;
internal set internal set
{ {
if (m_IsTogSelected == value) return;
m_IsTogSelected = value; m_IsTogSelected = value;
onValueChanged?.Invoke(m_IsTogSelected); onValueChanged?.Invoke(m_IsTogSelected);
m_SelectionState = m_IsTogSelected ? SelectionState.Selected : SelectionState.Normal; m_SelectionState = value ? SelectionState.Selected : SelectionState.Normal;
UpdateVisualState(m_SelectionState, false); UpdateVisualState(m_SelectionState, false);
} }
} }
public Button.ButtonClickedEvent onClick public Button.ButtonClickedEvent onClick
{ {
get { return m_OnClick; } get => m_OnClick;
set { m_OnClick = value; } set => m_OnClick = value;
} }
[SerializeField] private UnityEvent<bool> m_OnValueChanged = new UnityEvent<bool>(); [SerializeField] private UnityEvent<bool> m_OnValueChanged = new();
public UnityEvent<bool> onValueChanged public UnityEvent<bool> onValueChanged
{ {
get { return m_OnValueChanged; } get => m_OnValueChanged;
set { m_OnValueChanged = value; } set => m_OnValueChanged = value;
} }
#endregion
#region Unity Lifecycle
protected override void Awake() protected override void Awake()
{ {
base.Awake(); base.Awake();
if (m_Mode == ButtonModeType.Toggle) _waitTimeFadeDuration = new WaitForSeconds(
{ Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration));
onValueChanged?.Invoke(IsSelected);
} var triggers = m_TransitionData.animationTriggers;
AddTriggerID(triggers.normalTrigger);
AddTriggerID(triggers.highlightedTrigger);
AddTriggerID(triggers.pressedTrigger);
AddTriggerID(triggers.selectedTrigger);
AddTriggerID(triggers.disabledTrigger);
UpdateVisualState(m_SelectionState, true); UpdateVisualState(m_SelectionState, true);
} }
protected override void OnDestroy()
{
if (_resetRoutine != null)
{
StopCoroutine(_resetRoutine);
_resetRoutine = null;
}
base.OnDestroy();
}
#endregion
#region Event Handlers
void IPointerDownHandler.OnPointerDown(PointerEventData eventData) void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
{ {
if (!m_Interactable) return; if (!ShouldProcessEvent(eventData)) return;
if (eventData.button != PointerEventData.InputButton.Left) return;
m_IsDown = true; m_IsDown = true;
m_SelectionState = SelectionState.Pressed; m_SelectionState = SelectionState.Pressed;
UpdateVisualState(m_SelectionState, false); UpdateVisualState(m_SelectionState, false);
@ -127,29 +154,30 @@ public class UXButton : UIBehaviour, IButton,
void IPointerUpHandler.OnPointerUp(PointerEventData eventData) void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{ {
if (!m_Interactable) return; if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
return;
m_IsDown = false; m_IsDown = false;
if (m_IsTogSelected)
if (!m_IsTogSelected)
{ {
m_SelectionState = m_DownAndExistUI ? SelectionState.Normal : SelectionState.Highlighted; m_SelectionState = SelectionState.Selected;
UpdateVisualState(m_SelectionState, false);
} }
else else
{ {
m_SelectionState = SelectionState.Selected; m_SelectionState = m_DownAndExistUI ? SelectionState.Normal : SelectionState.Highlighted;
UpdateVisualState(m_SelectionState, false);
} }
UpdateVisualState(m_SelectionState, false);
} }
void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData) void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData)
{ {
if (!m_Interactable || CantTouch()) return; if (!ShouldProcessEvent(eventData)) return;
m_SelectionState = SelectionState.Highlighted;
m_DownAndExistUI = false; m_DownAndExistUI = false;
if (m_IsDown) return; if (m_IsDown) return;
m_SelectionState = SelectionState.Highlighted;
UpdateVisualState(m_SelectionState, false); UpdateVisualState(m_SelectionState, false);
PlayButtonSound(hoverAudioClip); PlayButtonSound(hoverAudioClip);
} }
@ -163,45 +191,72 @@ public class UXButton : UIBehaviour, IButton,
return; return;
} }
if (CantTouch()) m_SelectionState = IsSelected ? SelectionState.Selected : SelectionState.Normal;
{
return;
}
m_SelectionState = SelectionState.Normal;
UpdateVisualState(m_SelectionState, false); UpdateVisualState(m_SelectionState, false);
} }
private bool CantTouch() void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
{ {
return m_Mode == ButtonModeType.Toggle && m_IsTogSelected; if (eventData.button != PointerEventData.InputButton.Left || !m_Interactable)
return;
PlayButtonSound(clickAudioClip);
ProcessClick();
} }
void ISubmitHandler.OnSubmit(BaseEventData eventData)
{
UpdateVisualState(SelectionState.Pressed, false);
ProcessClick();
if (_resetRoutine != null)
StopCoroutine(OnFinishSubmit());
_resetRoutine = StartCoroutine(OnFinishSubmit());
}
#endregion
#region Public Methods
public void SetSelect(bool state, bool boardEvent = false) public void SetSelect(bool state, bool boardEvent = false)
{ {
if (m_Mode != ButtonModeType.Toggle) return; if (m_Mode != ButtonModeType.Toggle) return;
m_IsTogSelected = state; _boardEvent = boardEvent;
if (boardEvent) onValueChanged?.Invoke(m_IsTogSelected); IsSelected = state;
m_SelectionState = m_IsTogSelected ? SelectionState.Selected : SelectionState.Normal; }
UpdateVisualState(m_SelectionState, false);
#endregion
#region Private Methods
private bool ShouldProcessEvent(PointerEventData eventData)
{
return m_Interactable &&
eventData.button == PointerEventData.InputButton.Left &&
!(m_Mode == ButtonModeType.Toggle && IsSelected);
} }
private void ProcessClick() private void ProcessClick()
{ {
if (m_Mode == ButtonModeType.Normal) if (!_boardEvent)
{ {
UISystemProfilerApi.AddMarker("Button.onClick", this); _boardEvent = true;
onClick?.Invoke();
}
else
{
if (m_UXGroup)
{
m_UXGroup.NotifyButtonClicked(this);
return; return;
} }
_boardEvent = true;
if (m_Mode == ButtonModeType.Normal)
{
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick?.Invoke();
}
else if (m_UXGroup)
{
m_UXGroup.NotifyButtonClicked(this);
}
else
{
IsSelected = !IsSelected; IsSelected = !IsSelected;
} }
} }
@ -215,7 +270,6 @@ public class UXButton : UIBehaviour, IButton,
} }
} }
private void ProcessTransitionData(TransitionData transition, SelectionState state, bool instant) private void ProcessTransitionData(TransitionData transition, SelectionState state, bool instant)
{ {
if (transition.targetGraphic == null) return; if (transition.targetGraphic == null) return;
@ -223,11 +277,12 @@ public class UXButton : UIBehaviour, IButton,
Color tintColor; Color tintColor;
Sprite transitionSprite; Sprite transitionSprite;
string triggerName; string triggerName;
switch (state) switch (state)
{ {
case SelectionState.Normal: case SelectionState.Normal:
tintColor = transition.colors.normalColor; tintColor = transition.colors.normalColor;
transitionSprite = null; transitionSprite = transition.spriteState.highlightedSprite;
triggerName = transition.animationTriggers.normalTrigger; triggerName = transition.animationTriggers.normalTrigger;
break; break;
case SelectionState.Highlighted: case SelectionState.Highlighted:
@ -251,10 +306,7 @@ public class UXButton : UIBehaviour, IButton,
triggerName = transition.animationTriggers.disabledTrigger; triggerName = transition.animationTriggers.disabledTrigger;
break; break;
default: default:
tintColor = Color.black; return;
transitionSprite = null;
triggerName = string.Empty;
break;
} }
switch (transition.transition) switch (transition.transition)
@ -266,60 +318,83 @@ public class UXButton : UIBehaviour, IButton,
DoSpriteSwap(transition, transitionSprite); DoSpriteSwap(transition, transitionSprite);
break; break;
case Selectable.Transition.Animation: case Selectable.Transition.Animation:
TriggerAnimation(transition.animationTriggers, triggerName); TriggerAnimation(triggerName);
break; break;
} }
} }
protected void StartColorTween(TransitionData transitionData, Color targetColor, bool instant) private void StartColorTween(TransitionData data, Color targetColor, bool instant)
{ {
if (Application.isPlaying) if (Application.isPlaying)
{ {
transitionData.targetGraphic.CrossFadeColor(targetColor, instant ? 0f : transitionData.colors.fadeDuration, true, true); data.targetGraphic.CrossFadeColor(
targetColor,
instant ? 0f : data.colors.fadeDuration,
true,
true
);
} }
else else
{ {
transitionData.targetGraphic.canvasRenderer.SetColor(targetColor); data.targetGraphic.canvasRenderer.SetColor(targetColor);
} }
} }
protected void DoSpriteSwap(TransitionData transitionData, Sprite newSprite) private void DoSpriteSwap(TransitionData data, Sprite newSprite)
{ {
if (transitionData.targetGraphic is Image image) if (data.targetGraphic is Image image)
{ {
image.overrideSprite = newSprite; image.overrideSprite = newSprite;
} }
else if (transitionData.targetGraphic != null)
{
Log.Error($"Target Graphic must be Image for SpriteSwap. Object: {transitionData.targetGraphic.name}");
}
} }
void TriggerAnimation(AnimationTriggers animationTriggers, string triggername) private void TriggerAnimation(string trigger)
{ {
if (animator == null || !animator.isActiveAndEnabled || !animator.hasBoundPlayables || string.IsNullOrEmpty(triggername)) if (animator == null ||
!animator.isActiveAndEnabled ||
!animator.hasBoundPlayables ||
string.IsNullOrEmpty(trigger))
return; return;
animator.ResetTrigger(animationTriggers.normalTrigger); foreach (var resetTrigger in _animResetTriggerIDs.Keys)
animator.ResetTrigger(animationTriggers.highlightedTrigger);
animator.ResetTrigger(animationTriggers.pressedTrigger);
animator.ResetTrigger(animationTriggers.selectedTrigger);
animator.ResetTrigger(animationTriggers.disabledTrigger);
animator.SetTrigger(triggername);
}
protected void PlayButtonSound(AudioClip clip)
{ {
GameApp.Audio?.Play(AudioType.UISound, clip, false, GameApp.Audio.UISoundVolume); animator.ResetTrigger(_animTriggerIDs[resetTrigger]);
} }
if (_animTriggerIDs.TryGetValue(trigger, out int id))
public void OnPointerClick(PointerEventData eventData)
{ {
if (eventData.button != PointerEventData.InputButton.Left) animator.SetTrigger(id);
return;
PlayButtonSound(clickAudioClip);
ProcessClick();
} }
} }
private void AddTriggerID(string triggerName)
{
if (!string.IsNullOrEmpty(triggerName))
{
int id = Animator.StringToHash(triggerName);
if (!_animTriggerIDs.ContainsKey(triggerName))
{
_animTriggerIDs.Add(triggerName, id);
_animResetTriggerIDs.Add(triggerName, id);
}
}
}
private void PlayButtonSound(AudioClip clip)
{
if (clip == null || GameApp.Audio == null) return;
GameApp.Audio.Play(AudioType.UISound, clip, false, GameApp.Audio.UISoundVolume);
}
private IEnumerator OnFinishSubmit()
{
yield return _waitTimeFadeDuration;
UpdateVisualState(
m_IsTogSelected ? SelectionState.Selected : SelectionState.Normal,
false
);
_resetRoutine = null;
}
#endregion
}

View File

@ -1,5 +1,3 @@
// UXGroup.cs
using UnityEngine; using UnityEngine;
using UnityEngine.Events; using UnityEngine.Events;
using System.Collections.Generic; using System.Collections.Generic;
@ -10,11 +8,12 @@ using UnityEngine.EventSystems;
public class UXGroup : UIBehaviour public class UXGroup : UIBehaviour
{ {
[SerializeField] private bool m_AllowSwitchOff; [SerializeField] private bool m_AllowSwitchOff;
[ReadOnly] [SerializeField] private List<UXButton> m_Buttons = new List<UXButton>(); [ReadOnly, SerializeField] private List<UXButton> m_Buttons = new();
private UXButton currentUXButton = null; private UXButton _current;
private readonly HashSet<UXButton> _registeredButtons = new();
public UnityEvent<UXButton> onSelectedChanged = new UnityEvent<UXButton>(); public UnityEvent<UXButton> onSelectedChanged = new();
public bool allowSwitchOff public bool allowSwitchOff
{ {
@ -28,88 +27,84 @@ public class UXGroup : UIBehaviour
protected override void OnDestroy() protected override void OnDestroy()
{ {
base.OnDestroy(); foreach (var button in _registeredButtons)
m_Buttons.Clear();
currentUXButton = null;
}
protected override void Awake()
{ {
base.Awake(); if (button) button.IsSelected = false;
ValidateGroupState();
} }
m_Buttons.Clear();
_registeredButtons.Clear();
base.OnDestroy();
}
protected override void Awake() => ValidateGroupState();
public void RegisterButton(UXButton button) public void RegisterButton(UXButton button)
{ {
if (!m_Buttons.Contains(button)) if (!button || _registeredButtons.Contains(button)) return;
{
m_Buttons.Add(button); m_Buttons.Add(button);
_registeredButtons.Add(button);
if (button.IsSelected) if (button.IsSelected)
{ {
if (currentUXButton != null && currentUXButton != button) if (_current && _current != button)
{ _current.IsSelected = false;
currentUXButton.IsSelected = false; _current = button;
}
currentUXButton = button;
} }
ValidateGroupState(); ValidateGroupState();
} }
}
public void UnregisterButton(UXButton button) public void UnregisterButton(UXButton button)
{ {
if (m_Buttons.Contains(button)) if (!button || !_registeredButtons.Contains(button)) return;
{
m_Buttons.Remove(button); m_Buttons.Remove(button);
_registeredButtons.Remove(button);
button.IsSelected = false; button.IsSelected = false;
}
if (_current == button)
_current = null;
} }
internal void NotifyButtonClicked(UXButton clickedButton) internal void NotifyButtonClicked(UXButton clickedButton)
{ {
if (!clickedButton.IsSelected) if (clickedButton.IsSelected)
{
SetSelectedButton(clickedButton);
}
else
{ {
if (m_AllowSwitchOff) if (m_AllowSwitchOff)
SetSelectedButton(null); SetSelectedButton(null);
else if (currentUXButton != clickedButton) }
clickedButton.IsSelected = true; else
{
SetSelectedButton(clickedButton);
} }
} }
private void SetSelectedButton(UXButton targetButton) private void SetSelectedButton(UXButton target)
{ {
UXButton previousSelected = currentUXButton; var previous = _current;
currentUXButton = null; // 防止递归 _current = null;
foreach (var button in m_Buttons) foreach (var button in m_Buttons)
{ {
bool shouldSelect = (button == targetButton); bool shouldSelect = (button == target);
if (button.IsSelected != shouldSelect) if (button.IsSelected != shouldSelect)
{
button.IsSelected = shouldSelect; button.IsSelected = shouldSelect;
}
if (shouldSelect) currentUXButton = button; if (shouldSelect)
_current = button;
} }
if (previousSelected != currentUXButton) if (previous != _current)
{ onSelectedChanged?.Invoke(_current);
onSelectedChanged.Invoke(currentUXButton);
}
} }
private void ValidateGroupState() private void ValidateGroupState()
{ {
bool anySelected = m_Buttons.Exists(b => b.IsSelected); bool hasSelected = _current != null && _current.IsSelected;
if (!anySelected && m_Buttons.Count > 0 && !m_AllowSwitchOff) if (!hasSelected && m_Buttons.Count > 0 && !m_AllowSwitchOff)
{
SetSelectedButton(m_Buttons[0]); SetSelectedButton(m_Buttons[0]);
} }
}
public bool AnyOtherSelected(UXButton exclusion) public bool AnyOtherSelected(UXButton exclusion)
{ {
@ -118,7 +113,6 @@ public class UXGroup : UIBehaviour
if (button != exclusion && button.IsSelected) if (button != exclusion && button.IsSelected)
return true; return true;
} }
return false; return false;
} }
} }