2025-12-19 20:26:22 +08:00
|
|
|
|
using System;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
using System.Collections.Generic;
|
2025-12-19 20:26:22 +08:00
|
|
|
|
using System.Linq;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
using UnityEngine.EventSystems;
|
2025-12-19 20:26:22 +08:00
|
|
|
|
using UnityEngine;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
|
2025-12-09 20:30:11 +08:00
|
|
|
|
namespace UnityEngine.UI
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
[AddComponentMenu("UI/UXGroup", 31)]
|
2025-12-09 20:30:11 +08:00
|
|
|
|
[DisallowMultipleComponent]
|
|
|
|
|
|
public class UXGroup : UIBehaviour
|
|
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
[SerializeField] private bool m_AllowSwitchOff = false;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
2025-12-09 20:30:11 +08:00
|
|
|
|
public bool allowSwitchOff
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
get { return m_AllowSwitchOff; }
|
|
|
|
|
|
set { m_AllowSwitchOff = value; }
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
2025-10-13 20:20:01 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
private List<UXToggle> m_Toggles = new List<UXToggle>();
|
|
|
|
|
|
|
|
|
|
|
|
// 新增:默认选中的 Toggle(可在 Inspector 设置)
|
|
|
|
|
|
[SerializeField]
|
|
|
|
|
|
private UXToggle m_DefaultToggle;
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
public UXToggle defaultToggle
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
get { return m_DefaultToggle; }
|
|
|
|
|
|
set { m_DefaultToggle = value; EnsureValidState(); }
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
protected UXGroup()
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
}
|
2025-10-13 20:20:01 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
protected override void Start()
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureValidState();
|
|
|
|
|
|
base.Start();
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
2025-12-09 20:30:11 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
protected override void OnEnable()
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
EnsureValidState();
|
|
|
|
|
|
base.OnEnable();
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
private void ValidateToggleIsInGroup(UXToggle toggle)
|
2025-04-11 17:26:28 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
if (toggle == null || !m_Toggles.Contains(toggle))
|
|
|
|
|
|
throw new ArgumentException(string.Format("UXToggle {0} is not part of ToggleGroup {1}", new object[] { toggle, this }));
|
|
|
|
|
|
}
|
2025-12-09 20:30:11 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
public void NotifyToggleOn(UXToggle toggle, bool sendCallback = true)
|
|
|
|
|
|
{
|
|
|
|
|
|
ValidateToggleIsInGroup(toggle);
|
|
|
|
|
|
for (var i = 0; i < m_Toggles.Count; i++)
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
if (m_Toggles[i] == toggle)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (sendCallback)
|
|
|
|
|
|
m_Toggles[i].isOn = false;
|
|
|
|
|
|
else
|
|
|
|
|
|
m_Toggles[i].SetIsOnWithoutNotify(false);
|
2025-12-09 20:30:11 +08:00
|
|
|
|
}
|
2025-12-19 20:26:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void UnregisterToggle(UXToggle toggle)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (toggle == null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
if (m_Toggles.Contains(toggle))
|
|
|
|
|
|
m_Toggles.Remove(toggle);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果被移除的正好是默认选项,则清空默认项
|
|
|
|
|
|
if (m_DefaultToggle == toggle)
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
m_DefaultToggle = null;
|
2025-12-09 20:30:11 +08:00
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|
2025-07-28 13:04:49 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
public void RegisterToggle(UXToggle toggle)
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
if (toggle == null)
|
|
|
|
|
|
return;
|
2025-10-13 20:20:01 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
if (!m_Toggles.Contains(toggle))
|
|
|
|
|
|
m_Toggles.Add(toggle);
|
|
|
|
|
|
|
|
|
|
|
|
// 当组内已有其他开启项,并且不允许 all-off 时,
|
|
|
|
|
|
// 如果新加入的 toggle 本身为 on,则需要把它关闭以维持单选。
|
|
|
|
|
|
if (!allowSwitchOff)
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
// 如果已经有一个 active 的 toggle(且不是刚加入的这个),并且刚加入的 toggle isOn 为 true,则关闭刚加入的 toggle
|
|
|
|
|
|
var firstActive = GetFirstActiveToggle();
|
|
|
|
|
|
if (firstActive != null && firstActive != toggle && toggle.isOn)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 我们使用不触发回调的方式避免编辑时产生不必要的事件调用
|
|
|
|
|
|
toggle.SetIsOnWithoutNotify(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (firstActive == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 没有任何 active 且不允许 all-off:如果 group 指定了 defaultToggle,优先选中它(但仅当 default 在本组内且可交互)
|
|
|
|
|
|
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle))
|
|
|
|
|
|
{
|
|
|
|
|
|
var dt = m_DefaultToggle;
|
|
|
|
|
|
if (dt != null && dt != toggle)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 确保默认项被选中(editor/runtime 都适用)
|
|
|
|
|
|
dt.isOn = true;
|
|
|
|
|
|
NotifyToggleOn(dt);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (dt == toggle)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 新加入项就是默认项,确保它为 on
|
|
|
|
|
|
toggle.isOn = true;
|
|
|
|
|
|
NotifyToggleOn(toggle);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-09 20:30:11 +08:00
|
|
|
|
}
|
2025-12-19 20:26:22 +08:00
|
|
|
|
}
|
2025-12-09 20:30:11 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
// 新增:判断组里是否包含某 toggle(用于运行时/编辑器避免重复注册)
|
|
|
|
|
|
public bool ContainsToggle(UXToggle toggle)
|
|
|
|
|
|
{
|
|
|
|
|
|
return m_Toggles != null && m_Toggles.Contains(toggle);
|
2025-12-09 20:30:11 +08:00
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
// Ensure list consistency: clean nulls, and make sure every toggle in this list actually references this group.
|
|
|
|
|
|
public void EnsureValidState()
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
if (m_Toggles == null)
|
|
|
|
|
|
m_Toggles = new List<UXToggle>();
|
|
|
|
|
|
|
|
|
|
|
|
// Remove null references first
|
|
|
|
|
|
m_Toggles.RemoveAll(x => x == null);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果不允许 all-off,优先尝试选中 defaultToggle,否则选中第一个。
|
|
|
|
|
|
if (!allowSwitchOff && !AnyTogglesOn() && m_Toggles.Count != 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
UXToggle toSelect = null;
|
|
|
|
|
|
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle))
|
|
|
|
|
|
{
|
|
|
|
|
|
toSelect = m_DefaultToggle;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
toSelect = m_Toggles[0];
|
|
|
|
|
|
}
|
2025-08-08 20:56:31 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
if (toSelect != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
toSelect.isOn = true;
|
|
|
|
|
|
NotifyToggleOn(toSelect);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerable<UXToggle> activeToggles = ActiveToggles();
|
|
|
|
|
|
|
|
|
|
|
|
if (activeToggles.Count() > 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 如果 defaultToggle 是开启的,优先保留它
|
|
|
|
|
|
UXToggle firstActive = GetFirstActiveToggle();
|
|
|
|
|
|
|
|
|
|
|
|
foreach (UXToggle toggle in activeToggles)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (toggle == firstActive)
|
|
|
|
|
|
{
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toggle.isOn = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Synchronize each toggle's group reference to this group if necessary,
|
|
|
|
|
|
// but avoid causing re-ordering in cases where it's already consistent.
|
|
|
|
|
|
for (int i = 0; i < m_Toggles.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var t = m_Toggles[i];
|
|
|
|
|
|
if (t == null)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (t.group != this)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 使用 setter 会触发必要的注册/注销逻辑
|
|
|
|
|
|
t.group = this;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-12-09 20:30:11 +08:00
|
|
|
|
}
|
2025-08-08 20:56:31 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
public bool AnyTogglesOn()
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
return m_Toggles.Find(x => x != null && x.isOn) != null;
|
2025-12-09 20:30:11 +08:00
|
|
|
|
}
|
2025-08-08 20:56:31 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
public IEnumerable<UXToggle> ActiveToggles()
|
|
|
|
|
|
{
|
|
|
|
|
|
return m_Toggles.Where(x => x != null && x.isOn);
|
|
|
|
|
|
}
|
2025-08-08 20:56:31 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
public UXToggle GetFirstActiveToggle()
|
2025-08-11 14:45:27 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
// 优先返回 defaultToggle(如果它处于 on 且在组内)
|
|
|
|
|
|
if (m_DefaultToggle != null && m_Toggles.Contains(m_DefaultToggle) && m_DefaultToggle.isOn)
|
|
|
|
|
|
return m_DefaultToggle;
|
|
|
|
|
|
|
|
|
|
|
|
IEnumerable<UXToggle> activeToggles = ActiveToggles();
|
|
|
|
|
|
return activeToggles.Count() > 0 ? activeToggles.First() : null;
|
2025-08-08 20:56:31 +08:00
|
|
|
|
}
|
2025-08-11 14:45:27 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
public void SetAllTogglesOff(bool sendCallback = true)
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
bool oldAllowSwitchOff = m_AllowSwitchOff;
|
|
|
|
|
|
m_AllowSwitchOff = true;
|
2025-12-09 20:30:11 +08:00
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
if (sendCallback)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (var i = 0; i < m_Toggles.Count; i++)
|
|
|
|
|
|
if (m_Toggles[i] != null)
|
|
|
|
|
|
m_Toggles[i].isOn = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
2025-12-09 20:30:11 +08:00
|
|
|
|
{
|
2025-12-19 20:26:22 +08:00
|
|
|
|
for (var i = 0; i < m_Toggles.Count; i++)
|
|
|
|
|
|
if (m_Toggles[i] != null)
|
|
|
|
|
|
m_Toggles[i].SetIsOnWithoutNotify(false);
|
2025-12-09 20:30:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-19 20:26:22 +08:00
|
|
|
|
m_AllowSwitchOff = oldAllowSwitchOff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Next()
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectAdjacent(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Preview()
|
|
|
|
|
|
{
|
|
|
|
|
|
SelectAdjacent(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SelectAdjacent(bool forward)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m_Toggles == null || m_Toggles.Count == 0)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
UXToggle current = GetFirstActiveToggle();
|
|
|
|
|
|
int currentIndex = current != null ? m_Toggles.IndexOf(current) : -1;
|
|
|
|
|
|
|
|
|
|
|
|
int idx = currentIndex;
|
|
|
|
|
|
if (idx == -1 && !forward)
|
|
|
|
|
|
idx = 0;
|
|
|
|
|
|
|
|
|
|
|
|
for (int step = 0; step < m_Toggles.Count; step++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (forward)
|
|
|
|
|
|
{
|
|
|
|
|
|
idx = (idx + 1) % m_Toggles.Count;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
idx = (idx - 1 + m_Toggles.Count) % m_Toggles.Count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
UXToggle t = m_Toggles[idx];
|
|
|
|
|
|
if (t == null)
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (!t.IsActive())
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
if (!t.IsInteractable())
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
|
|
t.isOn = true;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-09 20:30:11 +08:00
|
|
|
|
}
|
2025-08-08 20:56:31 +08:00
|
|
|
|
}
|
2025-04-11 17:26:28 +08:00
|
|
|
|
}
|