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.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; public AnimationTriggers animationTriggers = new(); } internal enum SelectionState { Normal, Highlighted, Pressed, Selected, Disabled, } [ExecuteInEditMode] [DisallowMultipleComponent] public class UXButton : UIBehaviour, IButton, 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(); [SerializeField] private TransitionData m_TransitionData = new(); [SerializeField] private List 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 _animTriggerIDs = new(); private readonly Dictionary _animResetTriggerIDs = new(); #endregion #region Properties internal Animator animator { get { if (!_animator) _animator = GetComponent(); return _animator; } } public bool IsSelected { get => m_IsTogSelected; internal set { if (m_IsTogSelected == value) return; m_IsTogSelected = value; onValueChanged?.Invoke(m_IsTogSelected); m_SelectionState = value ? SelectionState.Selected : SelectionState.Normal; UpdateVisualState(m_SelectionState, false); } } public Button.ButtonClickedEvent onClick { get => m_OnClick; set => m_OnClick = value; } [SerializeField] private UnityEvent m_OnValueChanged = new(); public UnityEvent onValueChanged { get => m_OnValueChanged; set => m_OnValueChanged = value; } #endregion #region Unity Lifecycle protected override void Awake() { base.Awake(); _boardEvent = true; _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 (!ShouldProcessEvent(eventData)) return; m_IsDown = true; m_SelectionState = SelectionState.Pressed; UpdateVisualState(m_SelectionState, false); } void IPointerUpHandler.OnPointerUp(PointerEventData eventData) { if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left) return; m_IsDown = false; if (m_IsTogSelected) { m_SelectionState = SelectionState.Selected; } else { m_SelectionState = m_DownAndExistUI ? SelectionState.Normal : SelectionState.Highlighted; } UpdateVisualState(m_SelectionState, false); } void IPointerEnterHandler.OnPointerEnter(PointerEventData eventData) { if (!ShouldProcessEvent(eventData)) return; m_DownAndExistUI = false; if (m_IsDown) return; m_SelectionState = SelectionState.Highlighted; UpdateVisualState(m_SelectionState, false); PlayButtonSound(hoverAudioClip); } void IPointerExitHandler.OnPointerExit(PointerEventData eventData) { if (!m_Interactable) return; if (m_IsDown) { m_DownAndExistUI = true; return; } m_SelectionState = IsSelected ? SelectionState.Selected : SelectionState.Normal; UpdateVisualState(m_SelectionState, false); } void IPointerClickHandler.OnPointerClick(PointerEventData eventData) { 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; _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); m_OnClick?.Invoke(); } else if (m_UXGroup) { m_UXGroup.NotifyButtonClicked(this); } 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; string triggerName; switch (state) { case SelectionState.Normal: tintColor = transition.colors.normalColor; transitionSprite = transition.spriteState.highlightedSprite; triggerName = transition.animationTriggers.normalTrigger; break; case SelectionState.Highlighted: tintColor = transition.colors.highlightedColor; transitionSprite = transition.spriteState.highlightedSprite; triggerName = transition.animationTriggers.highlightedTrigger; break; case SelectionState.Pressed: tintColor = transition.colors.pressedColor; transitionSprite = transition.spriteState.pressedSprite; triggerName = transition.animationTriggers.pressedTrigger; break; case SelectionState.Selected: tintColor = transition.colors.selectedColor; transitionSprite = transition.spriteState.selectedSprite; triggerName = transition.animationTriggers.selectedTrigger; break; case SelectionState.Disabled: tintColor = transition.colors.disabledColor; transitionSprite = transition.spriteState.disabledSprite; triggerName = transition.animationTriggers.disabledTrigger; break; default: return; } switch (transition.transition) { case Selectable.Transition.ColorTint: StartColorTween(transition, tintColor * transition.colors.colorMultiplier, instant); break; case Selectable.Transition.SpriteSwap: DoSpriteSwap(transition, transitionSprite); break; case Selectable.Transition.Animation: TriggerAnimation(triggerName); break; } } private void StartColorTween(TransitionData data, Color targetColor, bool instant) { if (Application.isPlaying) { data.targetGraphic.CrossFadeColor( targetColor, instant ? 0f : data.colors.fadeDuration, true, true ); } else { data.targetGraphic.canvasRenderer.SetColor(targetColor); } } private void DoSpriteSwap(TransitionData data, Sprite newSprite) { if (data.targetGraphic is Image image) { image.overrideSprite = newSprite; } } private void TriggerAnimation(string trigger) { if (animator == null || !animator.isActiveAndEnabled || !animator.hasBoundPlayables || string.IsNullOrEmpty(trigger)) return; foreach (var resetTrigger in _animResetTriggerIDs.Keys) { animator.ResetTrigger(_animTriggerIDs[resetTrigger]); } if (_animTriggerIDs.TryGetValue(trigger, out int id)) { animator.SetTrigger(id); } } 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 }