using System; using System.Collections; using System.Collections.Generic; using AlicizaX.UI.Extension; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.UI; [Serializable] public enum ButtonModeType { Normal, Toggle } [ExecuteAlways] [DisallowMultipleComponent] public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler { #region Serialized Fields [SerializeField] private ButtonModeType m_Mode; [SerializeField] private Button.ButtonClickedEvent m_OnClick = 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 private bool m_IsDown; private bool m_HasExitedWhileDown; private bool _mTogSelected; private Coroutine _resetRoutine; private Coroutine _deferredDeselectRoutine; private WaitForSeconds _waitFadeDuration; // 静态锁(用于 normal 模式点击后的“保持 Selected”并能转移) private static UXButton s_LockedButton = null; private bool m_IsFocusLocked = false; private bool m_IsNavFocused = false; #endregion #region Properties 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); // 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected(无论逻辑是否为 selected)。 bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject; bool isFocused = m_IsNavFocused || isEventSystemSelected; if (m_Mode == ButtonModeType.Toggle && isFocused) { // 即使逻辑值为 false,也保持 Selected 视觉,因为焦点还在该控件上 SetState(SelectionState.Selected); } else { // 否则按逻辑恢复 Selected / Normal 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(); // 使用基类 m_MainTransition 的 fadeDuration _waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_MainTransition.colors.fadeDuration)); ApplyVisualState(m_SelectionState, true); } protected override void OnDestroy() { if (_resetRoutine != null) StopCoroutine(_resetRoutine); base.OnDestroy(); } public override bool IsInteractable() { return base.IsInteractable() && m_Interactable; } #endregion #region Static lock helper private static void SetLockedButton(UXButton newLocked) { if (s_LockedButton == newLocked) return; if (s_LockedButton != null) { var old = s_LockedButton; s_LockedButton = null; old.m_IsFocusLocked = false; if (old._mTogSelected && old.m_Mode == ButtonModeType.Toggle) old.SetState(SelectionState.Selected); } if (newLocked != null) { s_LockedButton = newLocked; s_LockedButton.m_IsFocusLocked = true; s_LockedButton.SetState(SelectionState.Selected); } } #endregion #region Pointer Handlers public override void OnPointerDown(PointerEventData eventData) { if (!CanProcess()) return; m_IsDown = true; m_HasExitedWhileDown = false; SetState(SelectionState.Pressed); } public override void OnPointerUp(PointerEventData eventData) { if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left) return; m_IsDown = false; if (m_IsFocusLocked || (_mTogSelected && m_Mode == ButtonModeType.Toggle && navigation.mode != UXNavigation.Mode.None)) { SetState(SelectionState.Selected); return; } var newState = m_HasExitedWhileDown ? SelectionState.Normal : SelectionState.Highlighted; SetState(newState); } public override void OnPointerEnter(PointerEventData eventData) { if (!CanProcessEnter()) return; m_HasExitedWhileDown = false; // 如果 toggle 模式并且聚焦(导航/或 EventSystem 选中),保持 Selected(不触发 Highlight) bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject; bool isFocused = m_IsNavFocused || isEventSystemSelected; if (m_Mode == ButtonModeType.Toggle && isFocused) { SetState(SelectionState.Selected); return; } if (navigation.mode != UXNavigation.Mode.None) { if ((m_Mode == ButtonModeType.Normal && m_IsFocusLocked) || (m_Mode == ButtonModeType.Toggle && _mTogSelected)) { SetState(SelectionState.Selected); return; } } if (m_IsDown) return; SetState(SelectionState.Highlighted); PlayAudio(hoverAudioClip); } public override void OnPointerExit(PointerEventData eventData) { if (!m_Interactable) return; if (m_IsDown) { m_HasExitedWhileDown = true; return; } // 聚焦时保持 Selected(不回退到 Normal) bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject; bool isFocused = m_IsNavFocused || isEventSystemSelected; if (m_Mode == ButtonModeType.Toggle && isFocused) { SetState(SelectionState.Selected); return; } if (navigation.mode != UXNavigation.Mode.None) { if ((m_Mode == ButtonModeType.Normal && m_IsFocusLocked) || (m_Mode == ButtonModeType.Toggle && _mTogSelected)) { SetState(SelectionState.Selected); return; } } SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal); } public void OnPointerClick(PointerEventData eventData) { if (eventData.button != PointerEventData.InputButton.Left || !m_Interactable) return; PlayAudio(clickAudioClip); if (m_Mode == ButtonModeType.Normal) { if (navigation.mode != UXNavigation.Mode.None) { if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(gameObject, eventData); UISystemProfilerApi.AddMarker("Button.onClick", this); m_OnClick?.Invoke(); if (IsStillSelected()) SetLockedButton(this); } else { UISystemProfilerApi.AddMarker("Button.onClick", this); m_OnClick?.Invoke(); } } else { HandleClick(); if (navigation.mode != UXNavigation.Mode.None) { if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(gameObject, eventData); if (IsStillSelected()) SetLockedButton(this); } } } #endregion #region Submit handling (Keyboard Submit) public void OnSubmit(BaseEventData eventData) { if (_resetRoutine != null) { StopCoroutine(_resetRoutine); _resetRoutine = null; } if (Animator) { foreach (int id in _animTriggerCache.Values) Animator.ResetTrigger(id); } if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(gameObject, eventData); m_IsDown = true; m_HasExitedWhileDown = false; ForceSetState(SelectionState.Pressed); PlayAudio(clickAudioClip); if (m_Mode == ButtonModeType.Toggle) { _resetRoutine = StartCoroutine(SubmitToggleDeferredRoutine()); return; } // ① 先执行回调(可能切走焦点) HandleClick(); // ② 只在“焦点仍在自己”时才锁 if (navigation.mode != UXNavigation.Mode.None && IsStillSelected()) SetLockedButton(this); if (navigation.mode != UXNavigation.Mode.None && m_Mode == ButtonModeType.Normal) _resetRoutine = StartCoroutine(SubmitAndLockRoutine()); else _resetRoutine = StartCoroutine(ResetAfterSubmit()); } #endregion #region Selection / Navigation handling public override void OnSelect(BaseEventData eventData) { if (m_Navigation.mode == UXNavigation.Mode.None) return; base.OnSelect(eventData); m_IsNavFocused = true; if (s_LockedButton != this) { SetLockedButton(this); return; } // 如果聚焦时(无论逻辑是否选中),Toggle 显示 Selected if (m_Mode == ButtonModeType.Toggle) { SetState(SelectionState.Selected); return; } if (m_IsDown) { SetState(SelectionState.Pressed); } else { if (m_IsFocusLocked) SetState(SelectionState.Selected); else SetState(SelectionState.Highlighted); } } private IEnumerator DeferredDeselectCheck() { yield return null; bool selectionIsNull = EventSystem.current == null || EventSystem.current.currentSelectedGameObject == null; if (selectionIsNull) { m_IsNavFocused = false; s_LockedButton = null; m_IsFocusLocked = false; } _deferredDeselectRoutine = null; } public override void OnDeselect(BaseEventData eventData) { base.OnDeselect(eventData); m_IsNavFocused = false; // 停掉上一次的延迟检查(若有) if (_deferredDeselectRoutine != null) { StopCoroutine(_deferredDeselectRoutine); _deferredDeselectRoutine = null; } // 延迟一帧再判断 EventSystem.current.currentSelectedGameObject,避免读取到旧值 _deferredDeselectRoutine = StartCoroutine(DeferredDeselectCheck()); // 视觉状态先按原逻辑处理(立即更新视觉),协程会确保锁状态在正确时刻被清理 if (m_Mode == ButtonModeType.Toggle) { if (_mTogSelected) SetState(SelectionState.Selected); else SetState(SelectionState.Normal); return; } if (m_Mode == ButtonModeType.Normal && _mTogSelected) { SetState(SelectionState.Selected); return; } SetState(SelectionState.Normal); } #endregion #region Coroutines for submit/reset private IEnumerator SubmitAndLockRoutine() { yield return _waitFadeDuration; m_IsDown = false; m_HasExitedWhileDown = false; if (Animator) { foreach (int id in _animTriggerCache.Values) Animator.ResetTrigger(id); } if (IsStillSelected()) { SetLockedButton(this); ApplyVisualState(SelectionState.Selected, false); } _resetRoutine = null; } private IEnumerator SubmitToggleDeferredRoutine() { yield return null; yield return _waitFadeDuration; HandleClick(); if (Animator) { foreach (int id in _animTriggerCache.Values) Animator.ResetTrigger(id); } m_IsDown = false; m_HasExitedWhileDown = false; bool stillFocused = IsStillSelected(); if (stillFocused) SetState(SelectionState.Selected); else SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal); if (navigation.mode != UXNavigation.Mode.None && stillFocused) SetLockedButton(this); _resetRoutine = null; } private IEnumerator ResetAfterSubmit() { yield return _waitFadeDuration; SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal); _resetRoutine = null; } #endregion #region Utility private bool IsStillSelected() { if (EventSystem.current != null) return EventSystem.current.currentSelectedGameObject == gameObject; return m_IsNavFocused; } #endregion #region Logic private bool CanProcess() { return m_Interactable; } private bool CanProcessEnter() { return m_Interactable; } 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 = !_mTogSelected; } } /// /// 设置状态的入口(使用基类 SetState) /// /// private void SetState(SelectionState state) { ForceSetState(state); ApplyVisualState(state, true); } public override void ApplyVisualState(SelectionState state, bool instant) { base.ApplyVisualState(state, instant); for (int i = 0; i < m_ChildTransitions.Count; i++) base.ApplyTransition(m_ChildTransitions[i], state, instant); } private void PlayAudio(AudioClip clip) { if (clip && UXComponentExtensionsHelper.AudioHelper != null) UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip); } #endregion protected override void OnSetProperty() { base.OnSetProperty(); ApplyVisualState(m_SelectionState, true); } public void Focus() { if (gameObject != null && EventSystem.current != null) { EventSystem.current.SetSelectedGameObject(gameObject); } } }