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_ContentOffset: {x: 15, y: 1}
m_FixedWidth: 24
m_FixedHeight: 15
m_FixedHeight: 24
m_StretchWidth: 1
m_StretchHeight: 0
m_label:

View File

@ -9,7 +9,7 @@ using UnityEngine.UI;
[CanEditMultipleObjects]
[CustomEditor(typeof(UXButton), true)]
public class UXButtonEditor : Editor
internal class UXButtonEditor : Editor
{
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
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
icon: {fileID: 2800000, guid: c4398d454b1a861499ef73d23bc7a032, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,11 +1,11 @@
using System;
using System.Collections;
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;
@ -16,7 +16,6 @@ public enum ButtonModeType
Toggle
}
[System.Serializable]
public class TransitionData
{
@ -24,102 +23,130 @@ public class TransitionData
public Selectable.Transition transition = Selectable.Transition.ColorTint;
public ColorBlock colors;
public SpriteState spriteState;
public AnimationTriggers animationTriggers = new AnimationTriggers();
public AnimationTriggers animationTriggers = new();
}
internal enum SelectionState
{
Normal,
Highlighted,
Pressed,
Selected,
Disabled,
}
[ExecuteInEditMode]
[DisallowMultipleComponent]
public class UXButton : UIBehaviour, IButton,
IPointerDownHandler,
IPointerUpHandler,
IPointerEnterHandler,
IPointerExitHandler,
IPointerClickHandler
IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler,
IPointerExitHandler, IPointerClickHandler, ISubmitHandler
{
#region Serialized Fields
[SerializeField] private bool m_Interactable = true;
[SerializeField] private ButtonModeType m_Mode;
[SerializeField] private Button.ButtonClickedEvent m_OnClick = new Button.ButtonClickedEvent();
[SerializeField] private TransitionData m_TransitionData = new TransitionData();
[SerializeField] private List<TransitionData> m_ChildTransitions = new List<TransitionData>();
[SerializeField] private Button.ButtonClickedEvent m_OnClick = new();
[SerializeField] private TransitionData m_TransitionData = new();
[SerializeField] private List<TransitionData> m_ChildTransitions = new();
[SerializeField] private UXGroup m_UXGroup;
[SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip;
#endregion
#region Private Variables
[SerializeField] private SelectionState m_SelectionState = SelectionState.Normal;
private bool m_DownAndExistUI;
private bool m_IsDown;
private bool m_IsTogSelected;
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
{
get
{
_animator = _animator ?? GetComponent<Animator>();
if (!_animator)
_animator = GetComponent<Animator>();
return _animator;
}
}
public bool IsSelected
{
get { return m_IsTogSelected; }
get => m_IsTogSelected;
internal set
{
if (m_IsTogSelected == value) return;
m_IsTogSelected = value;
onValueChanged?.Invoke(m_IsTogSelected);
m_SelectionState = m_IsTogSelected ? SelectionState.Selected : SelectionState.Normal;
m_SelectionState = value ? SelectionState.Selected : SelectionState.Normal;
UpdateVisualState(m_SelectionState, false);
}
}
public Button.ButtonClickedEvent onClick
{
get { return m_OnClick; }
set { m_OnClick = value; }
get => m_OnClick;
set => m_OnClick = value;
}
[SerializeField] private UnityEvent<bool> m_OnValueChanged = new UnityEvent<bool>();
[SerializeField] private UnityEvent<bool> m_OnValueChanged = new();
public UnityEvent<bool> onValueChanged
{
get { return m_OnValueChanged; }
set { m_OnValueChanged = value; }
get => m_OnValueChanged;
set => m_OnValueChanged = value;
}
#endregion
#region Unity Lifecycle
protected override void Awake()
{
base.Awake();
if (m_Mode == ButtonModeType.Toggle)
{
onValueChanged?.Invoke(IsSelected);
}
_waitTimeFadeDuration = new WaitForSeconds(
Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration));
var triggers = m_TransitionData.animationTriggers;
AddTriggerID(triggers.normalTrigger);
AddTriggerID(triggers.highlightedTrigger);
AddTriggerID(triggers.pressedTrigger);
AddTriggerID(triggers.selectedTrigger);
AddTriggerID(triggers.disabledTrigger);
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)
{
if (!m_Interactable) return;
if (eventData.button != PointerEventData.InputButton.Left) return;
if (!ShouldProcessEvent(eventData)) return;
m_IsDown = true;
m_SelectionState = SelectionState.Pressed;
UpdateVisualState(m_SelectionState, false);
@ -127,29 +154,30 @@ public class UXButton : UIBehaviour, IButton,
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
{
if (!m_Interactable) return;
if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
return;
m_IsDown = false;
if (!m_IsTogSelected)
if (m_IsTogSelected)
{
m_SelectionState = m_DownAndExistUI ? SelectionState.Normal : SelectionState.Highlighted;
UpdateVisualState(m_SelectionState, false);
m_SelectionState = SelectionState.Selected;
}
else
{
m_SelectionState = SelectionState.Selected;
UpdateVisualState(m_SelectionState, false);
m_SelectionState = m_DownAndExistUI ? SelectionState.Normal : SelectionState.Highlighted;
}
UpdateVisualState(m_SelectionState, false);
}
void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData)
{
if (!m_Interactable || CantTouch()) return;
if (!ShouldProcessEvent(eventData)) return;
m_SelectionState = SelectionState.Highlighted;
m_DownAndExistUI = false;
if (m_IsDown) return;
m_SelectionState = SelectionState.Highlighted;
UpdateVisualState(m_SelectionState, false);
PlayButtonSound(hoverAudioClip);
}
@ -163,45 +191,72 @@ public class UXButton : UIBehaviour, IButton,
return;
}
if (CantTouch())
{
return;
}
m_SelectionState = SelectionState.Normal;
m_SelectionState = IsSelected ? SelectionState.Selected : SelectionState.Normal;
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)
{
if (m_Mode != ButtonModeType.Toggle) return;
m_IsTogSelected = state;
if (boardEvent) onValueChanged?.Invoke(m_IsTogSelected);
m_SelectionState = m_IsTogSelected ? SelectionState.Selected : SelectionState.Normal;
UpdateVisualState(m_SelectionState, false);
_boardEvent = boardEvent;
IsSelected = state;
}
#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()
{
if (!_boardEvent)
{
_boardEvent = true;
return;
}
_boardEvent = true;
if (m_Mode == ButtonModeType.Normal)
{
UISystemProfilerApi.AddMarker("Button.onClick", this);
onClick?.Invoke();
m_OnClick?.Invoke();
}
else if (m_UXGroup)
{
m_UXGroup.NotifyButtonClicked(this);
}
else
{
if (m_UXGroup)
{
m_UXGroup.NotifyButtonClicked(this);
return;
}
IsSelected = !IsSelected;
}
}
@ -215,7 +270,6 @@ public class UXButton : UIBehaviour, IButton,
}
}
private void ProcessTransitionData(TransitionData transition, SelectionState state, bool instant)
{
if (transition.targetGraphic == null) return;
@ -223,11 +277,12 @@ public class UXButton : UIBehaviour, IButton,
Color tintColor;
Sprite transitionSprite;
string triggerName;
switch (state)
{
case SelectionState.Normal:
tintColor = transition.colors.normalColor;
transitionSprite = null;
transitionSprite = transition.spriteState.highlightedSprite;
triggerName = transition.animationTriggers.normalTrigger;
break;
case SelectionState.Highlighted:
@ -251,10 +306,7 @@ public class UXButton : UIBehaviour, IButton,
triggerName = transition.animationTriggers.disabledTrigger;
break;
default:
tintColor = Color.black;
transitionSprite = null;
triggerName = string.Empty;
break;
return;
}
switch (transition.transition)
@ -266,60 +318,83 @@ public class UXButton : UIBehaviour, IButton,
DoSpriteSwap(transition, transitionSprite);
break;
case Selectable.Transition.Animation:
TriggerAnimation(transition.animationTriggers, triggerName);
TriggerAnimation(triggerName);
break;
}
}
protected void StartColorTween(TransitionData transitionData, Color targetColor, bool instant)
private void StartColorTween(TransitionData data, Color targetColor, bool instant)
{
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
{
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;
}
else if (transitionData.targetGraphic != null)
}
private void TriggerAnimation(string trigger)
{
if (animator == null ||
!animator.isActiveAndEnabled ||
!animator.hasBoundPlayables ||
string.IsNullOrEmpty(trigger))
return;
foreach (var resetTrigger in _animResetTriggerIDs.Keys)
{
Log.Error($"Target Graphic must be Image for SpriteSwap. Object: {transitionData.targetGraphic.name}");
animator.ResetTrigger(_animTriggerIDs[resetTrigger]);
}
if (_animTriggerIDs.TryGetValue(trigger, out int id))
{
animator.SetTrigger(id);
}
}
void TriggerAnimation(AnimationTriggers animationTriggers, string triggername)
private void AddTriggerID(string triggerName)
{
if (animator == null || !animator.isActiveAndEnabled || !animator.hasBoundPlayables || string.IsNullOrEmpty(triggername))
return;
animator.ResetTrigger(animationTriggers.normalTrigger);
animator.ResetTrigger(animationTriggers.highlightedTrigger);
animator.ResetTrigger(animationTriggers.pressedTrigger);
animator.ResetTrigger(animationTriggers.selectedTrigger);
animator.ResetTrigger(animationTriggers.disabledTrigger);
animator.SetTrigger(triggername);
if (!string.IsNullOrEmpty(triggerName))
{
int id = Animator.StringToHash(triggerName);
if (!_animTriggerIDs.ContainsKey(triggerName))
{
_animTriggerIDs.Add(triggerName, id);
_animResetTriggerIDs.Add(triggerName, id);
}
}
}
protected void PlayButtonSound(AudioClip clip)
private void PlayButtonSound(AudioClip clip)
{
GameApp.Audio?.Play(AudioType.UISound, clip, false, GameApp.Audio.UISoundVolume);
if (clip == null || GameApp.Audio == null) return;
GameApp.Audio.Play(AudioType.UISound, clip, false, GameApp.Audio.UISoundVolume);
}
public void OnPointerClick(PointerEventData eventData)
private IEnumerator OnFinishSubmit()
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
PlayButtonSound(clickAudioClip);
ProcessClick();
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.Events;
using System.Collections.Generic;
@ -10,11 +8,12 @@ using UnityEngine.EventSystems;
public class UXGroup : UIBehaviour
{
[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
{
@ -28,87 +27,83 @@ public class UXGroup : UIBehaviour
protected override void OnDestroy()
{
base.OnDestroy();
foreach (var button in _registeredButtons)
{
if (button) button.IsSelected = false;
}
m_Buttons.Clear();
currentUXButton = null;
_registeredButtons.Clear();
base.OnDestroy();
}
protected override void Awake()
{
base.Awake();
ValidateGroupState();
}
protected override void Awake() => ValidateGroupState();
public void RegisterButton(UXButton button)
{
if (!m_Buttons.Contains(button))
if (!button || _registeredButtons.Contains(button)) return;
m_Buttons.Add(button);
_registeredButtons.Add(button);
if (button.IsSelected)
{
m_Buttons.Add(button);
if (button.IsSelected)
{
if (currentUXButton != null && currentUXButton != button)
{
currentUXButton.IsSelected = false;
}
currentUXButton = button;
}
ValidateGroupState();
if (_current && _current != button)
_current.IsSelected = false;
_current = button;
}
ValidateGroupState();
}
public void UnregisterButton(UXButton button)
{
if (m_Buttons.Contains(button))
{
m_Buttons.Remove(button);
button.IsSelected = false;
}
if (!button || !_registeredButtons.Contains(button)) return;
m_Buttons.Remove(button);
_registeredButtons.Remove(button);
button.IsSelected = false;
if (_current == button)
_current = null;
}
internal void NotifyButtonClicked(UXButton clickedButton)
{
if (!clickedButton.IsSelected)
{
SetSelectedButton(clickedButton);
}
else
if (clickedButton.IsSelected)
{
if (m_AllowSwitchOff)
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;
currentUXButton = null; // 防止递归
var previous = _current;
_current = null;
foreach (var button in m_Buttons)
{
bool shouldSelect = (button == targetButton);
bool shouldSelect = (button == target);
if (button.IsSelected != shouldSelect)
{
button.IsSelected = shouldSelect;
}
if (shouldSelect) currentUXButton = button;
if (shouldSelect)
_current = button;
}
if (previousSelected != currentUXButton)
{
onSelectedChanged.Invoke(currentUXButton);
}
if (previous != _current)
onSelectedChanged?.Invoke(_current);
}
private void ValidateGroupState()
{
bool anySelected = m_Buttons.Exists(b => b.IsSelected);
if (!anySelected && m_Buttons.Count > 0 && !m_AllowSwitchOff)
{
bool hasSelected = _current != null && _current.IsSelected;
if (!hasSelected && m_Buttons.Count > 0 && !m_AllowSwitchOff)
SetSelectedButton(m_Buttons[0]);
}
}
public bool AnyOtherSelected(UXButton exclusion)
@ -118,7 +113,6 @@ public class UXGroup : UIBehaviour
if (button != exclusion && button.IsSelected)
return true;
}
return false;
}
}