com.alicizax.unity.ui.exten.../Runtime/UXComponent/Button/UXButton.cs
2025-12-17 14:39:09 +08:00

638 lines
18 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 Coroutine _deferredLockRoutine; // 新增:用于 Focus 延迟锁定的协程
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);
if (_deferredDeselectRoutine != null)
StopCoroutine(_deferredDeselectRoutine);
if (_deferredLockRoutine != null)
StopCoroutine(_deferredLockRoutine);
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);
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)
{
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;
// 当前选中对象(可能为 null
var currentSelected = EventSystem.current != null ? EventSystem.current.currentSelectedGameObject : null;
if (currentSelected == null)
{
// 没有任何选中对象:清理锁
SetLockedButton(null);
m_IsFocusLocked = false;
}
else
{
// 如果有选中对象,检查它是否属于一个 UXButton可能是子对象
var selectedBtn = currentSelected.GetComponentInParent<UXButton>();
if (selectedBtn == null)
{
// 新选中对象不是 UXButton 的组成部分:清理锁
SetLockedButton(null);
m_IsFocusLocked = false;
}
else if (selectedBtn != this)
{
// 新选中对象属于另一个 UXButton把锁转给那个按钮
// (即便目标的 OnSelect 也可能会做这件事,这里直接转能避免时序问题)
SetLockedButton(selectedBtn);
}
else
{
// selectedBtn == this焦点仍然在自己上不需要清理
}
}
_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;
}
}
/// <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 (_deferredLockRoutine != null)
{
StopCoroutine(_deferredLockRoutine);
_deferredLockRoutine = null;
}
// 如果已经成为 EventSystem 的 selected则立即锁定否则延迟一帧再确认并锁定
if (IsStillSelected())
{
SetLockedButton(this);
}
else
{
_deferredLockRoutine = StartCoroutine(DeferredLockAfterFocus());
}
}
private IEnumerator DeferredLockAfterFocus()
{
// 等一帧,让 EventSystem 完成 selected 的更新和分发OnDeselect/OnSelect 等)
yield return null;
if (IsStillSelected())
SetLockedButton(this);
_deferredLockRoutine = null;
}
}