com.alicizax.unity.ui.exten.../Runtime/UXComponent/Button/UXButton.cs
2025-12-15 20:54:23 +08:00

585 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<TransitionData> m_ChildTransitions = new();
[SerializeField] private UXGroup m_UXGroup;
[SerializeField] private AudioClip hoverAudioClip;
[SerializeField] private AudioClip clickAudioClip;
[SerializeField] private UnityEvent<bool> 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<bool> 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);
else
old.SetState(SelectionState.Normal);
}
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);
SetLockedButton(this);
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick?.Invoke();
}
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);
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)
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)
{
base.OnSelect(eventData);
m_IsNavFocused = true;
if (s_LockedButton != null && 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()
{
// 等一帧,保证 EventSystem 更新 currentSelectedGameObject
yield return null;
bool selectionIsNull = EventSystem.current == null || EventSystem.current.currentSelectedGameObject == null;
if (selectionIsNull)
{
// 统一走封装入口清理锁
SetLockedButton(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);
}
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 = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
if (stillFocused || m_IsNavFocused)
SetState(SelectionState.Selected);
else
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
if (navigation.mode != UXNavigation.Mode.None)
SetLockedButton(this);
_resetRoutine = null;
}
private IEnumerator ResetAfterSubmit()
{
yield return _waitFadeDuration;
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
_resetRoutine = null;
}
#endregion
#region Utility
private void ForceSetState(SelectionState state)
{
// 链接到基类的 ForceSetState
base.ForceSetState(state);
}
#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;
}
}
/// <summary>
/// 设置状态的入口(使用基类 SetState
/// </summary>
/// <param name="state"></param>
private void SetState(SelectionState state)
{
ForceSetState(state);
ApplyVisualState(state, true);
}
/// <summary>
/// 覆盖基类的 ApplyVisualState先由基类处理 MainTransition再处理 child transitions保持原逻辑
/// </summary>
public override void ApplyVisualState(SelectionState state, bool instant)
{
// main transition 由基类处理
base.ApplyVisualState(state, instant);
// child transitions 保持原有处理UXButton 特有)
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 (!IsInteractable())
return;
if (EventSystem.current != null)
{
EventSystem.current.SetSelectedGameObject(gameObject, new BaseEventData(EventSystem.current));
}
m_IsNavFocused = true;
if ((s_LockedButton != null && s_LockedButton != this) || s_LockedButton == null)
{
SetLockedButton(this);
}
}
}