301 lines
8.7 KiB
C#
301 lines
8.7 KiB
C#
using System;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
using UnityEngine.EventSystems;
|
||
#if INPUTSYSTEM_SUPPORT
|
||
using UnityEngine.InputSystem;
|
||
#endif
|
||
#if UNITY_EDITOR
|
||
using UnityEditor;
|
||
#endif
|
||
|
||
namespace UnityEngine.UI
|
||
{
|
||
[AddComponentMenu("UI/UXToggle", 30)]
|
||
[RequireComponent(typeof(RectTransform))]
|
||
public class UXToggle : UXSelectable, IPointerClickHandler, ISubmitHandler, ICanvasElement
|
||
#if INPUTSYSTEM_SUPPORT
|
||
, IHotkeyTrigger
|
||
#endif
|
||
{
|
||
[Serializable]
|
||
public class ToggleEvent : UnityEvent<bool>
|
||
{
|
||
}
|
||
|
||
public Toggle.ToggleTransition toggleTransition = Toggle.ToggleTransition.Fade;
|
||
public Graphic graphic;
|
||
|
||
[SerializeField] private UXGroup m_Group;
|
||
|
||
public UXGroup group
|
||
{
|
||
get { return m_Group; }
|
||
set
|
||
{
|
||
SetToggleGroup(value, true);
|
||
PlayEffect(true);
|
||
}
|
||
}
|
||
|
||
public ToggleEvent onValueChanged = new ToggleEvent();
|
||
|
||
[Tooltip("Is the toggle currently on or off?")] [SerializeField]
|
||
private bool m_IsOn;
|
||
|
||
protected UXToggle()
|
||
{
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
protected override void OnValidate()
|
||
{
|
||
base.OnValidate();
|
||
|
||
if (!UnityEditor.PrefabUtility.IsPartOfPrefabAsset(this) && !Application.isPlaying)
|
||
CanvasUpdateRegistry.RegisterCanvasElementForLayoutRebuild(this);
|
||
}
|
||
#endif
|
||
|
||
public virtual void Rebuild(CanvasUpdate executing)
|
||
{
|
||
#if UNITY_EDITOR
|
||
if (executing == CanvasUpdate.Prelayout)
|
||
onValueChanged.Invoke(m_IsOn);
|
||
#endif
|
||
}
|
||
|
||
public virtual void LayoutComplete()
|
||
{
|
||
}
|
||
|
||
public virtual void GraphicUpdateComplete()
|
||
{
|
||
}
|
||
|
||
protected override void OnDestroy()
|
||
{
|
||
if (m_Group != null)
|
||
m_Group.EnsureValidState();
|
||
base.OnDestroy();
|
||
}
|
||
|
||
protected override void OnEnable()
|
||
{
|
||
base.OnEnable();
|
||
PlayEffect(true);
|
||
}
|
||
|
||
protected override void OnDidApplyAnimationProperties()
|
||
{
|
||
if (graphic != null)
|
||
{
|
||
bool oldValue = !Mathf.Approximately(graphic.canvasRenderer.GetColor().a, 0);
|
||
if (m_IsOn != oldValue)
|
||
{
|
||
m_IsOn = oldValue;
|
||
Set(!oldValue);
|
||
}
|
||
}
|
||
|
||
base.OnDidApplyAnimationProperties();
|
||
}
|
||
|
||
// Centralized group setter logic.
|
||
private void SetToggleGroup(UXGroup newGroup, bool setMemberValue)
|
||
{
|
||
// 如果组没有改变,仍然需要确保组里包含此 toggle(修复编辑器中批量拖拽只注册最后一项的问题)
|
||
if (m_Group == newGroup)
|
||
{
|
||
if (setMemberValue)
|
||
m_Group = newGroup;
|
||
|
||
if (newGroup != null && !newGroup.ContainsToggle(this))
|
||
{
|
||
newGroup.RegisterToggle(this);
|
||
}
|
||
|
||
// 尝试同步组状态,确保编辑器批量赋值时能稳定显示
|
||
if (newGroup != null)
|
||
newGroup.EnsureValidState();
|
||
|
||
return;
|
||
}
|
||
|
||
// 从旧组注销(如果存在)
|
||
if (m_Group != null)
|
||
m_Group.UnregisterToggle(this);
|
||
|
||
if (setMemberValue)
|
||
m_Group = newGroup;
|
||
|
||
// 注册到新组(不再强依赖 IsActive(),以保证编辑器批量赋值时也能正确注册)
|
||
if (newGroup != null)
|
||
{
|
||
if (!newGroup.ContainsToggle(this))
|
||
{
|
||
newGroup.RegisterToggle(this);
|
||
}
|
||
|
||
// 如果正在 on,通知组(维持单选逻辑)
|
||
if (isOn)
|
||
newGroup.NotifyToggleOn(this);
|
||
|
||
// 同步组的内部状态,确保 Inspector 列表正确显示
|
||
newGroup.EnsureValidState();
|
||
}
|
||
}
|
||
|
||
public bool isOn
|
||
{
|
||
get { return m_IsOn; }
|
||
set { Set(value); }
|
||
}
|
||
|
||
public void SetIsOnWithoutNotify(bool value)
|
||
{
|
||
Set(value, false);
|
||
}
|
||
|
||
// 在 UXToggle 类内(建议放在类底部较靠近 Set 和 PlayEffect 的位置)加入:
|
||
|
||
// 覆盖 DoStateTransition,保证 isOn 时视觉上总是 Selected(除非 Disabled)
|
||
protected override void DoStateTransition(Selectable.SelectionState state, bool instant)
|
||
{
|
||
// 如果被禁用,照常显示 Disabled
|
||
if (state == Selectable.SelectionState.Disabled)
|
||
{
|
||
base.DoStateTransition(state, instant);
|
||
return;
|
||
}
|
||
|
||
// 当 isOn 为 true 时,总是以 Selected 的样式渲染(但不改变 EventSystem 的真实 selection)
|
||
if (m_IsOn)
|
||
state = Selectable.SelectionState.Selected;
|
||
|
||
base.DoStateTransition(state, instant);
|
||
}
|
||
|
||
void Set(bool value, bool sendCallback = true)
|
||
{
|
||
if (m_IsOn == value)
|
||
return;
|
||
|
||
m_IsOn = value;
|
||
if (m_Group != null && m_Group.isActiveAndEnabled && IsActive())
|
||
{
|
||
if (m_IsOn || (!m_Group.AnyTogglesOn() && !m_Group.allowSwitchOff))
|
||
{
|
||
m_IsOn = true;
|
||
m_Group.NotifyToggleOn(this, sendCallback);
|
||
}
|
||
}
|
||
|
||
PlayEffect(toggleTransition == Toggle.ToggleTransition.None);
|
||
|
||
if (sendCallback)
|
||
{
|
||
UISystemProfilerApi.AddMarker("Toggle.value", this);
|
||
onValueChanged.Invoke(m_IsOn);
|
||
}
|
||
|
||
// 触发选择态视觉刷新(当 isOn 为 true 显示 Selected,false 时回到 normal/hover/... 的计算结果)
|
||
// instant 参数:如果 ToggleTransition 是 None 我们使用 instant,为保持和原来 PlayEffect 一致。
|
||
bool instant = (toggleTransition == Toggle.ToggleTransition.None);
|
||
// 传入的 state:当 isOn 为 true,DoStateTransition 会将其替换为 Selected;
|
||
// 当 isOn 为 false,使用 currentSelectionState 以让正常的鼠标/键盘状态生效。
|
||
var stateToApply = m_IsOn ? Selectable.SelectionState.Selected : currentSelectionState;
|
||
DoStateTransition(stateToApply, instant);
|
||
}
|
||
|
||
|
||
private void PlayEffect(bool instant)
|
||
{
|
||
if (graphic == null)
|
||
return;
|
||
|
||
#if UNITY_EDITOR
|
||
if (!Application.isPlaying)
|
||
graphic.canvasRenderer.SetAlpha(m_IsOn ? 1f : 0f);
|
||
else
|
||
#endif
|
||
graphic.CrossFadeAlpha(m_IsOn ? 1f : 0f, instant ? 0f : 0.1f, true);
|
||
}
|
||
|
||
|
||
protected override void Start()
|
||
{
|
||
PlayEffect(true);
|
||
}
|
||
|
||
private void InternalToggle()
|
||
{
|
||
if (!IsActive() || !IsInteractable())
|
||
return;
|
||
|
||
isOn = !isOn;
|
||
}
|
||
|
||
public override void OnPointerEnter(PointerEventData eventData)
|
||
{
|
||
base.OnPointerEnter(eventData);
|
||
PlayAudio(hoverAudioClip);
|
||
}
|
||
|
||
public virtual void OnPointerClick(PointerEventData eventData)
|
||
{
|
||
if (eventData.button != PointerEventData.InputButton.Left)
|
||
return;
|
||
|
||
InternalToggle();
|
||
PlayAudio(clickAudioClip);
|
||
}
|
||
|
||
public virtual void OnSubmit(BaseEventData eventData)
|
||
{
|
||
InternalToggle();
|
||
PlayAudio(clickAudioClip);
|
||
}
|
||
|
||
private void PlayAudio(AudioClip clip)
|
||
{
|
||
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
|
||
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
|
||
}
|
||
|
||
#if INPUTSYSTEM_SUPPORT
|
||
|
||
InputActionReference IHotkeyTrigger.HotkeyAction
|
||
{
|
||
get => _hotkeyAction;
|
||
set => _hotkeyAction = value;
|
||
}
|
||
|
||
EHotkeyPressType IHotkeyTrigger.HotkeyPressType
|
||
{
|
||
get => _hotkeyPressType;
|
||
set => _hotkeyPressType = value;
|
||
}
|
||
|
||
void IHotkeyTrigger.HotkeyActionTrigger()
|
||
{
|
||
if (interactable)
|
||
{
|
||
OnSubmit(null);
|
||
}
|
||
}
|
||
|
||
[SerializeField] internal InputActionReference _hotkeyAction;
|
||
[SerializeField] internal EHotkeyPressType _hotkeyPressType;
|
||
|
||
public InputActionReference HotKeyRefrence
|
||
{
|
||
get { return _hotkeyAction; }
|
||
}
|
||
#endif
|
||
|
||
[SerializeField] private AudioClip hoverAudioClip;
|
||
[SerializeField] private AudioClip clickAudioClip;
|
||
}
|
||
}
|