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

663 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
}
[Serializable]
public class TransitionData
{
public Graphic targetGraphic;
public Selectable.Transition transition = Selectable.Transition.ColorTint;
public ColorBlock colors = ColorBlock.defaultColorBlock;
public SpriteState spriteState;
public AnimationTriggers animationTriggers = new();
}
internal enum SelectionState
{
Normal,
Highlighted,
Pressed,
Selected,
Disabled,
}
[ExecuteInEditMode]
[DisallowMultipleComponent]
public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler
{
#region Serialized Fields
[SerializeField] private bool m_Interactable = true;
[SerializeField] private ButtonModeType m_Mode;
[SerializeField] private Button.ButtonClickedEvent m_OnClick = new();
[SerializeField] private TransitionData m_TransitionData = 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
[SerializeField] private SelectionState m_SelectionState = SelectionState.Normal;
private bool m_IsDown;
private bool m_HasExitedWhileDown;
private bool _mTogSelected;
private Animator _animator;
private Coroutine _resetRoutine;
private WaitForSeconds _waitFadeDuration;
// 静态锁(用于 normal 模式点击后的“保持 Selected”并能转移
private static UXButton s_LockedButton = null;
private bool m_IsFocusLocked = false;
private bool m_IsNavFocused = false;
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") },
};
#endregion
#region Properties
private Animator Animator => _animator ? _animator : _animator = GetComponent<Animator>();
public bool Interactable
{
get => m_Interactable;
set
{
if (m_Interactable == value) return;
m_Interactable = value;
SetState(m_Interactable ? SelectionState.Normal : SelectionState.Disabled);
}
}
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
// 聚焦判定使用 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);
}
}
}
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();
_waitFadeDuration = new WaitForSeconds(Mathf.Max(0.01f, m_TransitionData.colors.fadeDuration));
ApplyVisualState(m_SelectionState, true);
}
protected override void OnDestroy()
{
if (_resetRoutine != null)
StopCoroutine(_resetRoutine);
base.OnDestroy();
}
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);
}
}
#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);
}
}
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);
}
#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;
}
}
private void SetState(SelectionState state)
{
if (m_SelectionState == state) return;
m_SelectionState = state;
ApplyVisualState(state, false);
}
private void ApplyVisualState(SelectionState state, bool instant)
{
ApplyTransition(m_TransitionData, state, instant);
for (int i = 0; i < m_ChildTransitions.Count; i++)
ApplyTransition(m_ChildTransitions[i], state, instant);
}
private void ApplyTransition(TransitionData data, SelectionState state, bool instant)
{
if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation)
return;
(Color color, Sprite sprite, string trigger) = state switch
{
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)
{
case Selectable.Transition.ColorTint:
TweenColor(data, color * data.colors.colorMultiplier, instant);
break;
case Selectable.Transition.SpriteSwap:
SwapSprite(data, sprite);
break;
case Selectable.Transition.Animation:
PlayAnimation(trigger);
break;
}
}
private void TweenColor(TransitionData data, Color color, bool instant)
{
if (data.targetGraphic == null) return;
if (Application.isPlaying)
{
data.targetGraphic.CrossFadeColor(
color,
instant ? 0f : data.colors.fadeDuration,
true,
true
);
}
else
{
data.targetGraphic.canvasRenderer.SetColor(color);
}
}
private static void SwapSprite(TransitionData data, Sprite sprite)
{
if (data.targetGraphic is Image img)
img.overrideSprite = sprite;
}
private void PlayAnimation(string trigger)
{
if (!Animator || !Animator.isActiveAndEnabled || string.IsNullOrEmpty(trigger))
return;
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
if (_animTriggerCache.TryGetValue(trigger, out int hash))
Animator.SetTrigger(hash);
}
private void PlayAudio(AudioClip clip)
{
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
}
#endregion
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);
}
}
}