com.alicizax.unity.ui.exten.../Runtime/UXComponent/Button/UXButton.cs

663 lines
18 KiB
C#
Raw Normal View History

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-10-13 20:20:01 +08:00
[Serializable]
2025-04-11 17:26:28 +08:00
public class TransitionData
{
public Graphic targetGraphic;
public Selectable.Transition transition = Selectable.Transition.ColorTint;
2025-10-13 20:20:01 +08:00
public ColorBlock colors = ColorBlock.defaultColorBlock;
2025-04-11 17:26:28 +08:00
public SpriteState spriteState;
2025-07-28 13:04:49 +08:00
public AnimationTriggers animationTriggers = new();
2025-04-11 17:26:28 +08:00
}
internal enum SelectionState
{
Normal,
Highlighted,
Pressed,
Selected,
Disabled,
}
2025-07-29 11:28:24 +08:00
2025-07-28 13:04:49 +08:00
[ExecuteInEditMode]
2025-04-11 17:26:28 +08:00
[DisallowMultipleComponent]
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
2025-07-28 13:04:49 +08:00
[SerializeField] private bool m_Interactable = true;
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 TransitionData m_TransitionData = 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-07-25 13:29:20 +08:00
[SerializeField] private SelectionState m_SelectionState = SelectionState.Normal;
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
// 静态锁(用于 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 Interactable
2025-07-25 13:29:20 +08:00
{
2025-10-13 20:20:01 +08:00
get => m_Interactable;
set
2025-07-25 13:29:20 +08:00
{
2025-10-13 20:20:01 +08:00
if (m_Interactable == value) return;
m_Interactable = value;
SetState(m_Interactable ? SelectionState.Normal : SelectionState.Disabled);
2025-07-25 13:29:20 +08:00
}
}
2025-04-11 17:26:28 +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);
// ------------- 关键修复 -------------
// 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected无论逻辑是否为 selected
// 聚焦判定使用 m_IsNavFocusedOnSelect/OnDeselect 管理)或 EventSystem.current.currentSelectedGameObject == gameObject。
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-10-13 20:20:01 +08:00
_waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration));
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
}
protected override void OnSetProperty()
{
ApplyVisualState(m_SelectionState, true);
}
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
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;
m_HasExitedWhileDown = false;
2025-10-13 20:20:01 +08:00
SetState(SelectionState.Pressed);
2025-04-11 17:26:28 +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
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
}
public override void OnPointerEnter(PointerEventData eventData)
2025-04-11 17:26:28 +08:00
{
if (!CanProcessEnter()) return;
2025-10-13 20:20:01 +08:00
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;
}
}
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
}
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;
}
// 聚焦时保持 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);
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
}
#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-10-13 20:20:01 +08:00
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);
}
}
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)
{
m_SelectionState = state;
ApplyVisualState(state, false);
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
{
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
{
InternalTogSelected = !_mTogSelected;
2025-04-11 17:26:28 +08:00
}
}
2025-10-13 20:20:01 +08:00
private void SetState(SelectionState state)
2025-04-11 17:26:28 +08:00
{
2025-10-13 20:20:01 +08:00
if (m_SelectionState == state) return;
m_SelectionState = state;
ApplyVisualState(state, false);
2025-04-11 17:26:28 +08:00
}
2025-10-13 20:20:01 +08:00
private void ApplyVisualState(SelectionState state, bool instant)
2025-04-11 17:26:28 +08:00
{
2025-10-13 20:20:01 +08:00
ApplyTransition(m_TransitionData, state, instant);
for (int i = 0; i < m_ChildTransitions.Count; i++)
ApplyTransition(m_ChildTransitions[i], state, instant);
}
2025-04-11 17:26:28 +08:00
2025-10-13 20:20:01 +08:00
private void ApplyTransition(TransitionData data, SelectionState state, bool instant)
{
if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation)
return;
2025-07-28 13:04:49 +08:00
2025-10-13 20:20:01 +08:00
(Color color, Sprite sprite, string trigger) = state switch
2025-04-11 17:26:28 +08:00
{
2025-10-13 20:20:01 +08:00
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)
2025-04-11 17:26:28 +08:00
{
case Selectable.Transition.ColorTint:
2025-10-13 20:20:01 +08:00
TweenColor(data, color * data.colors.colorMultiplier, instant);
2025-04-11 17:26:28 +08:00
break;
case Selectable.Transition.SpriteSwap:
2025-10-13 20:20:01 +08:00
SwapSprite(data, sprite);
2025-04-11 17:26:28 +08:00
break;
2025-07-25 13:29:20 +08:00
case Selectable.Transition.Animation:
2025-10-13 20:20:01 +08:00
PlayAnimation(trigger);
2025-07-25 13:29:20 +08:00
break;
2025-04-11 17:26:28 +08:00
}
}
2025-10-13 20:20:01 +08:00
private void TweenColor(TransitionData data, Color color, bool instant)
2025-04-11 17:26:28 +08:00
{
if (data.targetGraphic == null) return;
2025-10-14 15:41:26 +08:00
if (Application.isPlaying)
{
data.targetGraphic.CrossFadeColor(
color,
instant ? 0f : data.colors.fadeDuration,
true,
true
);
}
else
{
data.targetGraphic.canvasRenderer.SetColor(color);
}
2025-04-11 17:26:28 +08:00
}
2025-10-13 20:20:01 +08:00
private static void SwapSprite(TransitionData data, Sprite sprite)
2025-04-11 17:26:28 +08:00
{
2025-10-13 20:20:01 +08:00
if (data.targetGraphic is Image img)
img.overrideSprite = sprite;
2025-04-11 17:26:28 +08:00
}
2025-10-13 20:20:01 +08:00
private 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
#endregion
public void Focus()
2025-04-17 16:03:39 +08:00
{
if (!IsInteractable())
return;
2025-07-28 13:04:49 +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
}