using System; using System.Collections; using System.Collections.Generic; using AlicizaX.UI.Extension; using AlicizaX.UI.Extension.Utility; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.UI; [Serializable] public enum ButtonModeType { Normal, Toggle } [Serializable] public class TransitionData { public Graphic targetGraphic; public Selectable.Transition transition = Selectable.Transition.ColorTint; public ColorBlock colors = ColorBlock.defaultColorBlock; 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; [SerializeField] private UnityEvent m_OnValueChanged = new(); #endregion #region Private Fields [SerializeField] private SelectionState m_SelectionState = SelectionState.Normal; private bool m_IsDown; private bool m_HasExitedWhileDown; private bool _mTogSelected; private Animator _animator; private Coroutine _resetRoutine; private WaitForSeconds _waitFadeDuration; private static readonly Dictionary _animTriggerCache = new() { { "Normal", Animator.StringToHash("Normal") }, { "Highlighted", Animator.StringToHash("Highlighted") }, { "Pressed", Animator.StringToHash("Pressed") }, { "Selected", Animator.StringToHash("Selected") }, { "Disabled", Animator.StringToHash("Disabled") }, }; #endregion #region Properties private Animator Animator => _animator ? _animator : _animator = GetComponent(); public bool Interactable { get => m_Interactable; set { if (m_Interactable == value) return; m_Interactable = value; SetState(m_Interactable ? SelectionState.Normal : SelectionState.Disabled); } } public bool Selected { get => _mTogSelected; set { if ((m_Mode == ButtonModeType.Normal && value) || m_Mode == ButtonModeType.Toggle) { if (m_Mode == ButtonModeType.Toggle) _mTogSelected = !value; HandleClick(); } } } internal bool InternalTogSelected { get => _mTogSelected; set { if (_mTogSelected == value) return; _mTogSelected = value; m_OnValueChanged?.Invoke(value); SetState(value ? SelectionState.Selected : SelectionState.Normal); } } public Button.ButtonClickedEvent onClick { get => m_OnClick; set => m_OnClick = value; } public UnityEvent onValueChanged { get => m_OnValueChanged; set => m_OnValueChanged = value; } #endregion #region Unity Lifecycle protected override void Awake() { base.Awake(); _waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration)); ApplyVisualState(m_SelectionState, true); } protected override void OnDestroy() { if (_resetRoutine != null) StopCoroutine(_resetRoutine); base.OnDestroy(); } #endregion #region Pointer Handlers public void OnPointerDown(PointerEventData eventData) { if (!CanProcess()) return; m_IsDown = true; SetState(SelectionState.Pressed); } public void OnPointerUp(PointerEventData eventData) { if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left) return; m_IsDown = false; var newState = _mTogSelected ? SelectionState.Selected : m_HasExitedWhileDown ? SelectionState.Normal : SelectionState.Highlighted; SetState(newState); } public void OnPointerEnter(PointerEventData eventData) { if (!CanProcess()) return; m_HasExitedWhileDown = false; if (m_IsDown) return; SetState(SelectionState.Highlighted); PlayAudio(hoverAudioClip); } public void OnPointerExit(PointerEventData eventData) { if (!m_Interactable) return; if (m_IsDown) { m_HasExitedWhileDown = true; return; } SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal); } public void OnPointerClick(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left || !m_Interactable) return; PlayAudio(clickAudioClip); HandleClick(); } public void OnSubmit(BaseEventData eventData) { SetState(SelectionState.Pressed); HandleClick(); if (_resetRoutine != null) StopCoroutine(_resetRoutine); _resetRoutine = StartCoroutine(ResetAfterSubmit()); } #endregion #region Logic private bool CanProcess() { return m_Interactable && !(m_Mode == ButtonModeType.Toggle && Selected); } private void HandleClick() { if (m_Mode == ButtonModeType.Normal) { UISystemProfilerApi.AddMarker("Button.onClick", this); m_OnClick?.Invoke(); } else if (m_UXGroup) { m_UXGroup.NotifyButtonClicked(this); } else { InternalTogSelected = !Selected; } } private void SetState(SelectionState state) { if (m_SelectionState == state) return; m_SelectionState = state; ApplyVisualState(state, false); } private void ApplyVisualState(SelectionState state, bool instant) { ApplyTransition(m_TransitionData, state, instant); for (int i = 0; i < m_ChildTransitions.Count; i++) ApplyTransition(m_ChildTransitions[i], state, instant); } private void ApplyTransition(TransitionData data, SelectionState state, bool instant) { if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation) return; (Color color, Sprite sprite, string trigger) = state switch { SelectionState.Normal => (data.colors.normalColor, data.spriteState.highlightedSprite, data.animationTriggers.normalTrigger), SelectionState.Highlighted => (data.colors.highlightedColor, data.spriteState.highlightedSprite, data.animationTriggers.highlightedTrigger), SelectionState.Pressed => (data.colors.pressedColor, data.spriteState.pressedSprite, data.animationTriggers.pressedTrigger), SelectionState.Selected => (data.colors.selectedColor, data.spriteState.selectedSprite, data.animationTriggers.selectedTrigger), SelectionState.Disabled => (data.colors.disabledColor, data.spriteState.disabledSprite, data.animationTriggers.disabledTrigger), _ => (Color.white, null, null) }; switch (data.transition) { case Selectable.Transition.ColorTint: TweenColor(data, color * data.colors.colorMultiplier, instant); break; case Selectable.Transition.SpriteSwap: SwapSprite(data, sprite); break; case Selectable.Transition.Animation: PlayAnimation(trigger); break; } } private void TweenColor(TransitionData data, Color color, bool instant) { if (Application.isPlaying) { data.targetGraphic.CrossFadeColor( color, instant ? 0f : data.colors.fadeDuration, true, true ); } else { data.targetGraphic.canvasRenderer.SetColor(color); } } private static void SwapSprite(TransitionData data, Sprite sprite) { if (data.targetGraphic is Image img) img.overrideSprite = sprite; } private void PlayAnimation(string trigger) { if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger)) return; foreach (int id in _animTriggerCache.Values) Animator.ResetTrigger(id); if (_animTriggerCache.TryGetValue(trigger, out int hash)) Animator.SetTrigger(hash); } private void PlayAudio(AudioClip clip) { if (clip && UXComponentExtensionsHelper.AudioHelper != null) UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip); } private IEnumerator ResetAfterSubmit() { yield return _waitFadeDuration; SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal); _resetRoutine = null; } #endregion }