2025-04-11 17:26:28 +08:00
|
|
|
|
using System;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
using System.Collections;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using AlicizaX.UI.Extension;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.Events;
|
|
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
|
2025-04-11 17:26:28 +08:00
|
|
|
|
[Serializable]
|
|
|
|
|
|
public enum ButtonModeType
|
|
|
|
|
|
{
|
|
|
|
|
|
Normal,
|
|
|
|
|
|
Toggle
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
|
[ExecuteAlways]
|
2025-04-11 17:26:28 +08:00
|
|
|
|
[DisallowMultipleComponent]
|
2025-12-03 17:26:55 +08:00
|
|
|
|
public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
|
#region Serialized Fields
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
|
|
|
|
|
[SerializeField] private ButtonModeType m_Mode;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
[SerializeField] private Button.ButtonClickedEvent m_OnClick = new();
|
|
|
|
|
|
[SerializeField] private List<TransitionData> m_ChildTransitions = new();
|
2025-04-11 17:26:28 +08:00
|
|
|
|
[SerializeField] private UXGroup m_UXGroup;
|
2025-07-25 19:53:34 +08:00
|
|
|
|
[SerializeField] private AudioClip hoverAudioClip;
|
|
|
|
|
|
[SerializeField] private AudioClip clickAudioClip;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
[SerializeField] private UnityEvent<bool> m_OnValueChanged = new();
|
2025-07-25 19:53:34 +08:00
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
#region Private Fields
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
2025-04-11 17:26:28 +08:00
|
|
|
|
private bool m_IsDown;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
private bool m_HasExitedWhileDown;
|
|
|
|
|
|
private bool _mTogSelected;
|
2025-07-25 13:29:20 +08:00
|
|
|
|
private Animator _animator;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
private Coroutine _resetRoutine;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
private WaitForSeconds _waitFadeDuration;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
// 静态锁(用于 normal 模式点击后的“保持 Selected”并能转移)
|
|
|
|
|
|
private static UXButton s_LockedButton = null;
|
|
|
|
|
|
|
|
|
|
|
|
private bool m_IsFocusLocked = false;
|
|
|
|
|
|
|
|
|
|
|
|
private bool m_IsNavFocused = false;
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
private static readonly Dictionary<string, int> _animTriggerCache = new()
|
|
|
|
|
|
{
|
|
|
|
|
|
{ "Normal", Animator.StringToHash("Normal") },
|
|
|
|
|
|
{ "Highlighted", Animator.StringToHash("Highlighted") },
|
|
|
|
|
|
{ "Pressed", Animator.StringToHash("Pressed") },
|
|
|
|
|
|
{ "Selected", Animator.StringToHash("Selected") },
|
|
|
|
|
|
{ "Disabled", Animator.StringToHash("Disabled") },
|
|
|
|
|
|
};
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Properties
|
2025-07-25 13:29:20 +08:00
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
private Animator Animator => _animator ? _animator : _animator = GetComponent<Animator>();
|
2025-08-08 20:56:31 +08:00
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
public bool Selected
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-10-13 20:20:01 +08:00
|
|
|
|
get => _mTogSelected;
|
2025-10-14 17:07:30 +08:00
|
|
|
|
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
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-10-13 20:20:01 +08:00
|
|
|
|
if (_mTogSelected == value) return;
|
|
|
|
|
|
_mTogSelected = value;
|
|
|
|
|
|
m_OnValueChanged?.Invoke(value);
|
2025-12-03 17:26:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果当前控件处于聚焦(由导航/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);
|
|
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-17 16:03:39 +08:00
|
|
|
|
public Button.ButtonClickedEvent onClick
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
|
get => m_OnClick;
|
|
|
|
|
|
set => m_OnClick = value;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public UnityEvent<bool> onValueChanged
|
|
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
|
get => m_OnValueChanged;
|
|
|
|
|
|
set => m_OnValueChanged = value;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Unity Lifecycle
|
|
|
|
|
|
|
2025-07-29 17:34:54 +08:00
|
|
|
|
protected override void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.Awake();
|
2025-12-09 15:08:41 +08:00
|
|
|
|
// 使用基类 m_MainTransition 的 fadeDuration
|
|
|
|
|
|
_waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_MainTransition.colors.fadeDuration));
|
2025-10-13 20:20:01 +08:00
|
|
|
|
ApplyVisualState(m_SelectionState, true);
|
2025-07-29 17:34:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
|
protected override void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_resetRoutine != null)
|
|
|
|
|
|
StopCoroutine(_resetRoutine);
|
|
|
|
|
|
base.OnDestroy();
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-28 13:04:49 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
#region Pointer Handlers
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
public override void OnPointerDown(PointerEventData eventData)
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-10-14 15:41:26 +08:00
|
|
|
|
if (!CanProcess()) return;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
m_IsDown = true;
|
2025-12-03 17:26:55 +08:00
|
|
|
|
m_HasExitedWhileDown = false;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
SetState(SelectionState.Pressed);
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
public override void OnPointerUp(PointerEventData eventData)
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
|
if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
|
|
|
|
|
|
return;
|
2025-04-17 16:03:39 +08:00
|
|
|
|
|
2025-04-11 17:26:28 +08:00
|
|
|
|
m_IsDown = false;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
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;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
SetState(newState);
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
public override void OnPointerEnter(PointerEventData eventData)
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-12-03 17:26:55 +08:00
|
|
|
|
if (!CanProcessEnter()) return;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
m_HasExitedWhileDown = false;
|
2025-12-03 17:26:55 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-17 16:03:39 +08:00
|
|
|
|
if (m_IsDown) return;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
SetState(SelectionState.Highlighted);
|
|
|
|
|
|
PlayAudio(hoverAudioClip);
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
public override void OnPointerExit(PointerEventData eventData)
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (!m_Interactable) return;
|
|
|
|
|
|
if (m_IsDown)
|
|
|
|
|
|
{
|
2025-10-13 20:20:01 +08:00
|
|
|
|
m_HasExitedWhileDown = true;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
// 聚焦时保持 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
|
2025-07-28 13:04:49 +08:00
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
public void OnPointerClick(PointerEventData eventData)
|
2025-07-28 13:04:49 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (eventData.button != PointerEventData.InputButton.Left || !m_Interactable)
|
|
|
|
|
|
return;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
PlayAudio(clickAudioClip);
|
2025-12-03 17:26:55 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Submit handling (Keyboard Submit)
|
|
|
|
|
|
|
2025-08-08 16:25:09 +08:00
|
|
|
|
public void OnSubmit(BaseEventData eventData)
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-07-28 13:04:49 +08:00
|
|
|
|
if (_resetRoutine != null)
|
2025-12-03 17:26:55 +08:00
|
|
|
|
{
|
2025-10-13 20:20:01 +08:00
|
|
|
|
StopCoroutine(_resetRoutine);
|
2025-12-03 17:26:55 +08:00
|
|
|
|
_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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void OnDeselect(BaseEventData eventData)
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnDeselect(eventData);
|
|
|
|
|
|
|
|
|
|
|
|
m_IsNavFocused = false;
|
|
|
|
|
|
|
|
|
|
|
|
bool selectionIsNull = EventSystem.current == null || EventSystem.current.currentSelectedGameObject == null;
|
|
|
|
|
|
if (selectionIsNull)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (s_LockedButton == this)
|
|
|
|
|
|
s_LockedButton = null;
|
|
|
|
|
|
m_IsFocusLocked = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle 模式:如果逻辑上已选中则保持 Selected,否则恢复 Normal
|
|
|
|
|
|
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)
|
|
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
|
// 链接到基类的 ForceSetState
|
|
|
|
|
|
base.ForceSetState(state);
|
2025-07-28 13:04:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
#region Logic
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
2025-10-14 15:41:26 +08:00
|
|
|
|
private bool CanProcess()
|
2025-07-28 13:04:49 +08:00
|
|
|
|
{
|
2025-12-03 17:26:55 +08:00
|
|
|
|
return m_Interactable;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private bool CanProcessEnter()
|
|
|
|
|
|
{
|
|
|
|
|
|
return m_Interactable;
|
2025-04-11 17:33:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
private void HandleClick()
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (m_Mode == ButtonModeType.Normal)
|
|
|
|
|
|
{
|
2025-04-17 16:03:39 +08:00
|
|
|
|
UISystemProfilerApi.AddMarker("Button.onClick", this);
|
2025-07-28 13:04:49 +08:00
|
|
|
|
m_OnClick?.Invoke();
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (m_UXGroup)
|
|
|
|
|
|
{
|
|
|
|
|
|
m_UXGroup.NotifyButtonClicked(this);
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-12-03 17:26:55 +08:00
|
|
|
|
InternalTogSelected = !_mTogSelected;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 设置状态的入口(使用基类 SetState)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="state"></param>
|
2025-10-13 20:20:01 +08:00
|
|
|
|
private void SetState(SelectionState state)
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
|
ForceSetState(state);
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 覆盖基类的 ApplyVisualState:先由基类处理 MainTransition,再处理 child transitions(保持原逻辑)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public override void ApplyVisualState(SelectionState state, bool instant)
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
|
// main transition 由基类处理
|
|
|
|
|
|
base.ApplyVisualState(state, instant);
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
|
// child transitions 保持原有处理(UXButton 特有)
|
|
|
|
|
|
for (int i = 0; i < m_ChildTransitions.Count; i++)
|
|
|
|
|
|
base.ApplyTransition(m_ChildTransitions[i], state, instant);
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 覆盖 PlayAnimation:使用 UXButton 的 Animator 与 trigger 缓存(保持原逻辑)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="trigger"></param>
|
|
|
|
|
|
protected override void PlayAnimation(string trigger)
|
2025-07-25 13:29:20 +08:00
|
|
|
|
{
|
2025-10-13 20:20:01 +08:00
|
|
|
|
if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger))
|
2025-07-25 13:29:20 +08:00
|
|
|
|
return;
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
foreach (int id in _animTriggerCache.Values)
|
|
|
|
|
|
Animator.ResetTrigger(id);
|
2025-07-25 13:29:20 +08:00
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
if (_animTriggerCache.TryGetValue(trigger, out int hash))
|
|
|
|
|
|
Animator.SetTrigger(hash);
|
2025-07-25 13:29:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-13 20:20:01 +08:00
|
|
|
|
private void PlayAudio(AudioClip clip)
|
2025-07-28 13:04:49 +08:00
|
|
|
|
{
|
2025-10-13 20:20:01 +08:00
|
|
|
|
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
|
|
|
|
|
|
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
|
2025-07-28 13:04:49 +08:00
|
|
|
|
}
|
2025-04-17 16:03:39 +08:00
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void Focus()
|
2025-04-17 16:03:39 +08:00
|
|
|
|
{
|
2025-12-03 17:26:55 +08:00
|
|
|
|
if (!IsInteractable())
|
|
|
|
|
|
return;
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|