2026-03-26 16:12:50 +08:00
|
|
|
#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
|
2026-03-20 16:46:06 +08:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using AlicizaX.UI.Runtime;
|
|
|
|
|
|
|
|
|
|
namespace UnityEngine.UI
|
|
|
|
|
{
|
|
|
|
|
[DisallowMultipleComponent]
|
|
|
|
|
[AddComponentMenu("UI/UX Navigation Scope")]
|
|
|
|
|
public sealed class UXNavigationScope : MonoBehaviour
|
|
|
|
|
{
|
|
|
|
|
[SerializeField, Header("默认选中控件")] private Selectable _defaultSelectable;
|
|
|
|
|
|
|
|
|
|
[SerializeField, Header("显式导航控件列表")] private List<Selectable> _explicitSelectables = new();
|
|
|
|
|
|
|
|
|
|
[SerializeField, Header("记住上次选中")] private bool _rememberLastSelection = true;
|
|
|
|
|
|
|
|
|
|
[SerializeField, Header("手柄模式必须有选中")] private bool _requireSelectionWhenGamepad = true;
|
|
|
|
|
|
|
|
|
|
[SerializeField, Header("阻断下层导航域")] private bool _blockLowerScopes = true;
|
|
|
|
|
|
|
|
|
|
[SerializeField, Header("自动选中首个可用控件")] private bool _autoSelectFirstAvailable = true;
|
|
|
|
|
|
|
|
|
|
private readonly List<Selectable> _cachedSelectables = new(16);
|
|
|
|
|
private readonly Dictionary<Selectable, Navigation> _baselineNavigation = new();
|
|
|
|
|
private readonly List<Selectable> _removeBuffer = new(8);
|
|
|
|
|
|
|
|
|
|
private Canvas _cachedCanvas;
|
|
|
|
|
private UIHolderObjectBase _cachedHolder;
|
|
|
|
|
private Selectable _lastSelected;
|
|
|
|
|
private bool _cacheDirty = true;
|
|
|
|
|
private bool _navigationSuppressed;
|
|
|
|
|
|
|
|
|
|
internal ulong ActivationSerial { get; set; }
|
|
|
|
|
internal bool WasAvailable { get; set; }
|
|
|
|
|
|
|
|
|
|
public Selectable DefaultSelectable
|
|
|
|
|
{
|
|
|
|
|
get => _defaultSelectable;
|
|
|
|
|
set => _defaultSelectable = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool RememberLastSelection => _rememberLastSelection;
|
|
|
|
|
public bool RequireSelectionWhenGamepad => _requireSelectionWhenGamepad;
|
|
|
|
|
public bool BlockLowerScopes => _blockLowerScopes;
|
|
|
|
|
|
|
|
|
|
internal Canvas Canvas
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (_cachedCanvas == null)
|
|
|
|
|
{
|
|
|
|
|
_cachedCanvas = GetComponentInParent<Canvas>(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _cachedCanvas;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal UIHolderObjectBase Holder
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (_cachedHolder == null)
|
|
|
|
|
{
|
|
|
|
|
_cachedHolder = GetComponent<UIHolderObjectBase>();
|
|
|
|
|
if (_cachedHolder == null)
|
|
|
|
|
{
|
|
|
|
|
_cachedHolder = GetComponentInParent<UIHolderObjectBase>(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return _cachedHolder;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
{
|
|
|
|
|
_cacheDirty = true;
|
|
|
|
|
UXNavigationRuntime.EnsureInstance().RegisterScope(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDisable()
|
|
|
|
|
{
|
|
|
|
|
SetNavigationSuppressed(false);
|
|
|
|
|
if (UXNavigationRuntime.TryGetInstance(out var runtime))
|
|
|
|
|
{
|
|
|
|
|
runtime.UnregisterScope(this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDestroy()
|
|
|
|
|
{
|
|
|
|
|
SetNavigationSuppressed(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnTransformChildrenChanged()
|
|
|
|
|
{
|
|
|
|
|
_cacheDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if UNITY_EDITOR
|
|
|
|
|
private void OnValidate()
|
|
|
|
|
{
|
|
|
|
|
_cacheDirty = true;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
internal int GetHierarchyDepth()
|
|
|
|
|
{
|
|
|
|
|
int depth = 0;
|
|
|
|
|
Transform current = transform;
|
|
|
|
|
while (current != null)
|
|
|
|
|
{
|
|
|
|
|
depth++;
|
|
|
|
|
current = current.parent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return depth;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal bool Owns(GameObject target)
|
|
|
|
|
{
|
|
|
|
|
if (target == null)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var nearestScope = target.GetComponentInParent<UXNavigationScope>(true);
|
|
|
|
|
return nearestScope == this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal Selectable GetPreferredSelectable()
|
|
|
|
|
{
|
|
|
|
|
RefreshSelectableCache();
|
|
|
|
|
|
|
|
|
|
if (_rememberLastSelection && IsSelectableValid(_lastSelected))
|
|
|
|
|
{
|
|
|
|
|
return _lastSelected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsSelectableValid(_defaultSelectable))
|
|
|
|
|
{
|
|
|
|
|
return _defaultSelectable;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!_autoSelectFirstAvailable)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < _cachedSelectables.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
Selectable selectable = _cachedSelectables[i];
|
|
|
|
|
if (IsSelectableValid(selectable))
|
|
|
|
|
{
|
|
|
|
|
return selectable;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 19:40:12 +08:00
|
|
|
internal bool HasAvailableSelectable()
|
|
|
|
|
{
|
|
|
|
|
RefreshSelectableCache();
|
|
|
|
|
|
|
|
|
|
if (IsSelectableValid(_defaultSelectable))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < _cachedSelectables.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
if (IsSelectableValid(_cachedSelectables[i]))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 16:46:06 +08:00
|
|
|
internal void RecordSelection(GameObject selectedObject)
|
|
|
|
|
{
|
|
|
|
|
if (!_rememberLastSelection || selectedObject == null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Selectable selectable = selectedObject.GetComponent<Selectable>();
|
|
|
|
|
if (selectable == null)
|
|
|
|
|
{
|
|
|
|
|
selectable = selectedObject.GetComponentInParent<Selectable>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsSelectableValid(selectable))
|
|
|
|
|
{
|
|
|
|
|
_lastSelected = selectable;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal bool IsSelectableOwnedAndValid(GameObject selectedObject)
|
|
|
|
|
{
|
|
|
|
|
if (selectedObject == null || !Owns(selectedObject))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Selectable selectable = selectedObject.GetComponent<Selectable>();
|
|
|
|
|
if (selectable == null)
|
|
|
|
|
{
|
|
|
|
|
selectable = selectedObject.GetComponentInParent<Selectable>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return IsSelectableValid(selectable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void SetNavigationSuppressed(bool suppressed)
|
|
|
|
|
{
|
|
|
|
|
RefreshSelectableCache();
|
|
|
|
|
if (_navigationSuppressed == suppressed)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_navigationSuppressed = suppressed;
|
|
|
|
|
for (int i = 0; i < _cachedSelectables.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
Selectable selectable = _cachedSelectables[i];
|
|
|
|
|
if (selectable == null)
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!_baselineNavigation.ContainsKey(selectable))
|
|
|
|
|
{
|
|
|
|
|
_baselineNavigation[selectable] = selectable.navigation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (suppressed)
|
|
|
|
|
{
|
|
|
|
|
Navigation navigation = selectable.navigation;
|
|
|
|
|
navigation.mode = Navigation.Mode.None;
|
|
|
|
|
selectable.navigation = navigation;
|
|
|
|
|
}
|
|
|
|
|
else if (_baselineNavigation.TryGetValue(selectable, out Navigation navigation))
|
|
|
|
|
{
|
|
|
|
|
selectable.navigation = navigation;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void RefreshSelectableCache()
|
|
|
|
|
{
|
|
|
|
|
if (!_cacheDirty)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_cacheDirty = false;
|
|
|
|
|
_cachedSelectables.Clear();
|
|
|
|
|
|
|
|
|
|
if (_explicitSelectables != null && _explicitSelectables.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < _explicitSelectables.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
TryAddSelectable(_explicitSelectables[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Selectable[] selectables = GetComponentsInChildren<Selectable>(true);
|
|
|
|
|
for (int i = 0; i < selectables.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
TryAddSelectable(selectables[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_removeBuffer.Clear();
|
|
|
|
|
foreach (var pair in _baselineNavigation)
|
|
|
|
|
{
|
|
|
|
|
if (!_cachedSelectables.Contains(pair.Key))
|
|
|
|
|
{
|
|
|
|
|
_removeBuffer.Add(pair.Key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < _removeBuffer.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
_baselineNavigation.Remove(_removeBuffer[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal void InvalidateSelectableCache()
|
|
|
|
|
{
|
|
|
|
|
_cacheDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TryAddSelectable(Selectable selectable)
|
|
|
|
|
{
|
|
|
|
|
if (selectable == null || !Owns(selectable.gameObject))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_cachedSelectables.Add(selectable);
|
|
|
|
|
if (!_baselineNavigation.ContainsKey(selectable) || !_navigationSuppressed)
|
|
|
|
|
{
|
|
|
|
|
_baselineNavigation[selectable] = selectable.navigation;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsSelectableValid(Selectable selectable)
|
|
|
|
|
{
|
|
|
|
|
return selectable != null
|
|
|
|
|
&& selectable.IsActive()
|
|
|
|
|
&& selectable.IsInteractable()
|
|
|
|
|
&& Owns(selectable.gameObject);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|