2025-04-11 17:26:28 +08:00
|
|
|
using System;
|
2025-07-28 13:04:49 +08:00
|
|
|
using System.Collections;
|
2025-04-11 17:26:28 +08:00
|
|
|
using System.Collections.Generic;
|
|
|
|
using AlicizaX;
|
|
|
|
using AlicizaX.UI.Extension;
|
|
|
|
using UnityEngine;
|
|
|
|
using UnityEngine.Events;
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
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;
|
2025-07-28 13:04:49 +08:00
|
|
|
public AnimationTriggers animationTriggers = new();
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
internal enum SelectionState
|
|
|
|
{
|
|
|
|
Normal,
|
|
|
|
Highlighted,
|
|
|
|
Pressed,
|
|
|
|
Selected,
|
|
|
|
Disabled,
|
|
|
|
}
|
2025-07-29 11:28:24 +08:00
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
[ExecuteInEditMode]
|
2025-04-11 17:26:28 +08:00
|
|
|
[DisallowMultipleComponent]
|
2025-04-17 16:03:39 +08:00
|
|
|
public class UXButton : UIBehaviour, IButton,
|
2025-07-28 13:04:49 +08:00
|
|
|
IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler,
|
|
|
|
IPointerExitHandler, IPointerClickHandler, ISubmitHandler
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
#region Serialized Fields
|
2025-04-11 17:26:28 +08:00
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
[SerializeField] private bool m_Interactable = true;
|
2025-04-11 17:26:28 +08:00
|
|
|
[SerializeField] private ButtonModeType m_Mode;
|
2025-07-28 13:04:49 +08:00
|
|
|
[SerializeField] private Button.ButtonClickedEvent m_OnClick = new();
|
|
|
|
[SerializeField] private TransitionData m_TransitionData = new();
|
|
|
|
[SerializeField] private List<TransitionData> m_ChildTransitions = new();
|
2025-04-11 17:26:28 +08:00
|
|
|
[SerializeField] private UXGroup m_UXGroup;
|
2025-07-25 19:53:34 +08:00
|
|
|
[SerializeField] private AudioClip hoverAudioClip;
|
|
|
|
[SerializeField] private AudioClip clickAudioClip;
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Private Variables
|
|
|
|
|
2025-07-25 13:29:20 +08:00
|
|
|
[SerializeField] private SelectionState m_SelectionState = SelectionState.Normal;
|
2025-04-17 16:03:39 +08:00
|
|
|
private bool m_DownAndExistUI;
|
2025-04-11 17:26:28 +08:00
|
|
|
private bool m_IsDown;
|
|
|
|
private bool m_IsTogSelected;
|
2025-07-25 13:29:20 +08:00
|
|
|
private Animator _animator;
|
2025-07-28 13:04:49 +08:00
|
|
|
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
|
2025-07-25 13:29:20 +08:00
|
|
|
|
|
|
|
internal Animator animator
|
|
|
|
{
|
|
|
|
get
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (!_animator)
|
|
|
|
_animator = GetComponent<Animator>();
|
2025-07-25 13:29:20 +08:00
|
|
|
return _animator;
|
|
|
|
}
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
|
|
public bool IsSelected
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
get => m_IsTogSelected;
|
2025-04-11 17:26:28 +08:00
|
|
|
internal set
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (m_IsTogSelected == value) return;
|
2025-04-11 17:26:28 +08:00
|
|
|
m_IsTogSelected = value;
|
|
|
|
onValueChanged?.Invoke(m_IsTogSelected);
|
2025-07-28 13:04:49 +08:00
|
|
|
m_SelectionState = value ? SelectionState.Selected : SelectionState.Normal;
|
2025-04-11 17:26:28 +08:00
|
|
|
UpdateVisualState(m_SelectionState, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-17 16:03:39 +08:00
|
|
|
public Button.ButtonClickedEvent onClick
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
get => m_OnClick;
|
|
|
|
set => m_OnClick = value;
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
[SerializeField] private UnityEvent<bool> m_OnValueChanged = new();
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
|
|
public UnityEvent<bool> onValueChanged
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
get => m_OnValueChanged;
|
|
|
|
set => m_OnValueChanged = value;
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Unity Lifecycle
|
|
|
|
|
2025-04-11 17:26:28 +08:00
|
|
|
protected override void Awake()
|
|
|
|
{
|
|
|
|
base.Awake();
|
2025-07-29 14:22:35 +08:00
|
|
|
_boardEvent = true;
|
2025-07-28 13:04:49 +08:00
|
|
|
_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)
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
StopCoroutine(_resetRoutine);
|
|
|
|
_resetRoutine = null;
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
2025-07-25 13:29:20 +08:00
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
base.OnDestroy();
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Event Handlers
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
|
|
void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (!ShouldProcessEvent(eventData)) return;
|
|
|
|
|
2025-04-11 17:26:28 +08:00
|
|
|
m_IsDown = true;
|
|
|
|
m_SelectionState = SelectionState.Pressed;
|
|
|
|
UpdateVisualState(m_SelectionState, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
void IPointerUpHandler.OnPointerUp(PointerEventData eventData)
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
|
|
|
|
return;
|
2025-04-17 16:03:39 +08:00
|
|
|
|
2025-04-11 17:26:28 +08:00
|
|
|
m_IsDown = false;
|
2025-07-28 13:04:49 +08:00
|
|
|
if (m_IsTogSelected)
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
m_SelectionState = SelectionState.Selected;
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
m_SelectionState = m_DownAndExistUI ? SelectionState.Normal : SelectionState.Highlighted;
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
|
|
UpdateVisualState(m_SelectionState, false);
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData)
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (!ShouldProcessEvent(eventData)) return;
|
2025-04-17 16:03:39 +08:00
|
|
|
|
|
|
|
m_DownAndExistUI = false;
|
|
|
|
if (m_IsDown) return;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
|
|
m_SelectionState = SelectionState.Highlighted;
|
2025-04-11 17:26:28 +08:00
|
|
|
UpdateVisualState(m_SelectionState, false);
|
2025-07-25 19:53:34 +08:00
|
|
|
PlayButtonSound(hoverAudioClip);
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void IPointerExitHandler.OnPointerExit(PointerEventData eventData)
|
|
|
|
{
|
|
|
|
if (!m_Interactable) return;
|
|
|
|
if (m_IsDown)
|
|
|
|
{
|
2025-04-17 16:03:39 +08:00
|
|
|
m_DownAndExistUI = true;
|
2025-04-11 17:26:28 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
m_SelectionState = IsSelected ? SelectionState.Selected : SelectionState.Normal;
|
|
|
|
UpdateVisualState(m_SelectionState, false);
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
|
|
|
|
{
|
|
|
|
if (eventData.button != PointerEventData.InputButton.Left || !m_Interactable)
|
|
|
|
return;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
PlayButtonSound(clickAudioClip);
|
|
|
|
ProcessClick();
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
void ISubmitHandler.OnSubmit(BaseEventData eventData)
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
UpdateVisualState(SelectionState.Pressed, false);
|
|
|
|
ProcessClick();
|
|
|
|
|
|
|
|
if (_resetRoutine != null)
|
|
|
|
StopCoroutine(OnFinishSubmit());
|
|
|
|
|
|
|
|
_resetRoutine = StartCoroutine(OnFinishSubmit());
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Public Methods
|
|
|
|
|
2025-04-15 17:27:57 +08:00
|
|
|
public void SetSelect(bool state, bool boardEvent = false)
|
2025-04-11 17:33:58 +08:00
|
|
|
{
|
|
|
|
if (m_Mode != ButtonModeType.Toggle) return;
|
2025-07-28 13:04:49 +08:00
|
|
|
_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);
|
2025-04-11 17:33:58 +08:00
|
|
|
}
|
|
|
|
|
2025-04-11 17:26:28 +08:00
|
|
|
private void ProcessClick()
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (!_boardEvent)
|
|
|
|
{
|
|
|
|
_boardEvent = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
_boardEvent = true;
|
2025-04-11 17:26:28 +08:00
|
|
|
if (m_Mode == ButtonModeType.Normal)
|
|
|
|
{
|
2025-04-17 16:03:39 +08:00
|
|
|
UISystemProfilerApi.AddMarker("Button.onClick", this);
|
2025-07-28 13:04:49 +08:00
|
|
|
m_OnClick?.Invoke();
|
|
|
|
}
|
|
|
|
else if (m_UXGroup)
|
|
|
|
{
|
|
|
|
m_UXGroup.NotifyButtonClicked(this);
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
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;
|
2025-07-25 13:29:20 +08:00
|
|
|
string triggerName;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
2025-04-11 17:26:28 +08:00
|
|
|
switch (state)
|
|
|
|
{
|
|
|
|
case SelectionState.Normal:
|
|
|
|
tintColor = transition.colors.normalColor;
|
2025-07-28 13:04:49 +08:00
|
|
|
transitionSprite = transition.spriteState.highlightedSprite;
|
2025-07-25 13:29:20 +08:00
|
|
|
triggerName = transition.animationTriggers.normalTrigger;
|
2025-04-11 17:26:28 +08:00
|
|
|
break;
|
|
|
|
case SelectionState.Highlighted:
|
|
|
|
tintColor = transition.colors.highlightedColor;
|
|
|
|
transitionSprite = transition.spriteState.highlightedSprite;
|
2025-07-25 13:29:20 +08:00
|
|
|
triggerName = transition.animationTriggers.highlightedTrigger;
|
2025-04-11 17:26:28 +08:00
|
|
|
break;
|
|
|
|
case SelectionState.Pressed:
|
|
|
|
tintColor = transition.colors.pressedColor;
|
|
|
|
transitionSprite = transition.spriteState.pressedSprite;
|
2025-07-25 13:29:20 +08:00
|
|
|
triggerName = transition.animationTriggers.pressedTrigger;
|
2025-04-11 17:26:28 +08:00
|
|
|
break;
|
|
|
|
case SelectionState.Selected:
|
|
|
|
tintColor = transition.colors.selectedColor;
|
|
|
|
transitionSprite = transition.spriteState.selectedSprite;
|
2025-07-25 13:29:20 +08:00
|
|
|
triggerName = transition.animationTriggers.selectedTrigger;
|
2025-04-11 17:26:28 +08:00
|
|
|
break;
|
|
|
|
case SelectionState.Disabled:
|
|
|
|
tintColor = transition.colors.disabledColor;
|
|
|
|
transitionSprite = transition.spriteState.disabledSprite;
|
2025-07-25 13:29:20 +08:00
|
|
|
triggerName = transition.animationTriggers.disabledTrigger;
|
2025-04-11 17:26:28 +08:00
|
|
|
break;
|
|
|
|
default:
|
2025-07-28 13:04:49 +08:00
|
|
|
return;
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
switch (transition.transition)
|
|
|
|
{
|
|
|
|
case Selectable.Transition.ColorTint:
|
|
|
|
StartColorTween(transition, tintColor * transition.colors.colorMultiplier, instant);
|
|
|
|
break;
|
|
|
|
case Selectable.Transition.SpriteSwap:
|
|
|
|
DoSpriteSwap(transition, transitionSprite);
|
|
|
|
break;
|
2025-07-25 13:29:20 +08:00
|
|
|
case Selectable.Transition.Animation:
|
2025-07-28 13:04:49 +08:00
|
|
|
TriggerAnimation(triggerName);
|
2025-07-25 13:29:20 +08:00
|
|
|
break;
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
private void StartColorTween(TransitionData data, Color targetColor, bool instant)
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
|
|
|
if (Application.isPlaying)
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
data.targetGraphic.CrossFadeColor(
|
|
|
|
targetColor,
|
|
|
|
instant ? 0f : data.colors.fadeDuration,
|
|
|
|
true,
|
|
|
|
true
|
|
|
|
);
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
data.targetGraphic.canvasRenderer.SetColor(targetColor);
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
private void DoSpriteSwap(TransitionData data, Sprite newSprite)
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (data.targetGraphic is Image image)
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
|
|
|
image.overrideSprite = newSprite;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
private void TriggerAnimation(string trigger)
|
2025-07-25 13:29:20 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (animator == null ||
|
|
|
|
!animator.isActiveAndEnabled ||
|
|
|
|
!animator.hasBoundPlayables ||
|
|
|
|
string.IsNullOrEmpty(trigger))
|
2025-07-25 13:29:20 +08:00
|
|
|
return;
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
foreach (var resetTrigger in _animResetTriggerIDs.Keys)
|
|
|
|
{
|
|
|
|
animator.ResetTrigger(_animTriggerIDs[resetTrigger]);
|
|
|
|
}
|
2025-07-25 13:29:20 +08:00
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
if (_animTriggerIDs.TryGetValue(trigger, out int id))
|
|
|
|
{
|
|
|
|
animator.SetTrigger(id);
|
|
|
|
}
|
2025-07-25 13:29:20 +08:00
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
private void AddTriggerID(string triggerName)
|
2025-04-11 17:26:28 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
if (!string.IsNullOrEmpty(triggerName))
|
|
|
|
{
|
|
|
|
int id = Animator.StringToHash(triggerName);
|
|
|
|
if (!_animTriggerIDs.ContainsKey(triggerName))
|
|
|
|
{
|
|
|
|
_animTriggerIDs.Add(triggerName, id);
|
|
|
|
_animResetTriggerIDs.Add(triggerName, id);
|
|
|
|
}
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
private void PlayButtonSound(AudioClip clip)
|
|
|
|
{
|
|
|
|
if (clip == null || GameApp.Audio == null) return;
|
|
|
|
GameApp.Audio.Play(AudioType.UISound, clip, false, GameApp.Audio.UISoundVolume);
|
|
|
|
}
|
2025-04-17 16:03:39 +08:00
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
private IEnumerator OnFinishSubmit()
|
2025-04-17 16:03:39 +08:00
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
yield return _waitTimeFadeDuration;
|
|
|
|
UpdateVisualState(
|
|
|
|
m_IsTogSelected ? SelectionState.Selected : SelectionState.Normal,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
_resetRoutine = null;
|
2025-04-17 16:03:39 +08:00
|
|
|
}
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
|
|
#endregion
|
2025-04-11 17:26:28 +08:00
|
|
|
}
|