com.alicizax.unity.ui.exten.../Runtime/UXComponent/Navigation/UXNavigationScope.cs

487 lines
15 KiB
C#
Raw Normal View History

#if INPUTSYSTEM_SUPPORT && UX_NAVIGATION
using AlicizaX.UI.Runtime;
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
[DisallowMultipleComponent]
[AddComponentMenu("UI/UX Navigation Scope")]
public sealed class UXNavigationScope : MonoBehaviour, ISelectHandler
{
private const int InvalidIndex = -1;
private const int DefaultRuntimeSelectableCapacity = 16;
[SerializeField, Header("默认选中控件")] private Selectable _defaultSelectable;
[SerializeField, Header("编辑器烘焙导航控件")] private Selectable[] _bakedSelectables = System.Array.Empty<Selectable>();
[SerializeField, Header("动态控件容量")] private int _runtimeSelectableCapacity = DefaultRuntimeSelectableCapacity;
[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 Selectable[] _runtimeSelectables;
private Navigation[] _bakedBaselineNavigation;
private Navigation[] _runtimeBaselineNavigation;
private Canvas _cachedCanvas;
private UIHolderObjectBase _cachedHolder;
private Selectable _lastSelected;
private bool _navigationSuppressed;
2026-04-09 15:21:49 +08:00
private int _cachedHierarchyDepth = -1;
2026-04-15 14:23:30 +08:00
private bool _cachedIsSkipped;
private bool _isSkippedCacheValid;
private int _runtimeSelectableCount;
2026-04-15 14:23:30 +08:00
internal int RuntimeIndex { get; set; } = InvalidIndex;
internal ulong ActivationSerial { get; set; }
2026-04-09 15:21:49 +08:00
internal bool IsAvailable { get; set; }
internal bool WasAvailable { get; set; }
public bool NavigationSuppressed => _navigationSuppressed;
internal int BakedSelectableCount => _bakedSelectables != null ? _bakedSelectables.Length : 0;
public int RuntimeSelectableCount => _runtimeSelectableCount;
public Selectable DefaultSelectable
{
get => _defaultSelectable;
set
{
_defaultSelectable = value;
MarkRuntimeStateDirty();
}
}
public bool RememberLastSelection => _rememberLastSelection;
public bool RequireSelectionWhenGamepad => _requireSelectionWhenGamepad;
public bool BlockLowerScopes => _blockLowerScopes;
2026-04-15 14:23:30 +08:00
internal bool IsNavigationSkipped
{
get
{
if (!_isSkippedCacheValid)
{
_cachedIsSkipped = GetComponentInParent<UXNavigationSkip>(true) != null;
_isSkippedCacheValid = true;
}
2026-04-15 14:23:30 +08:00
return _cachedIsSkipped;
}
}
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 Awake()
{
EnsureRuntimeBuffers();
CaptureAllBaselines();
}
private void OnEnable()
{
EnsureRuntimeBuffers();
CaptureAllBaselines();
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()
{
MarkRuntimeStateDirty();
}
2026-04-09 15:21:49 +08:00
private void OnTransformParentChanged()
{
_cachedCanvas = null;
_cachedHolder = null;
_cachedHierarchyDepth = -1;
2026-04-15 14:23:30 +08:00
_isSkippedCacheValid = false;
MarkRuntimeStateDirty();
2026-04-09 15:21:49 +08:00
}
#if UNITY_EDITOR
private void OnValidate()
{
if (_runtimeSelectableCapacity < 0)
{
_runtimeSelectableCapacity = 0;
}
}
#endif
public bool RegisterSelectable(Selectable selectable)
{
if (selectable == null || !Owns(selectable.gameObject) || ContainsSelectable(selectable))
{
return false;
}
EnsureRuntimeBuffers();
if (_runtimeSelectableCount >= _runtimeSelectables.Length)
{
ReportCapacityExceeded();
return false;
}
_runtimeSelectables[_runtimeSelectableCount] = selectable;
_runtimeBaselineNavigation[_runtimeSelectableCount] = selectable.navigation;
_runtimeSelectableCount++;
if (_navigationSuppressed)
{
SetSelectableSuppressed(selectable, true);
}
MarkRuntimeStateDirty();
return true;
}
public bool UnregisterSelectable(Selectable selectable)
{
if (selectable == null || _runtimeSelectables == null)
{
return false;
}
for (int i = 0; i < _runtimeSelectableCount; i++)
{
if (_runtimeSelectables[i] != selectable)
{
continue;
}
if (_navigationSuppressed)
{
selectable.navigation = _runtimeBaselineNavigation[i];
}
int last = _runtimeSelectableCount - 1;
_runtimeSelectables[i] = _runtimeSelectables[last];
_runtimeBaselineNavigation[i] = _runtimeBaselineNavigation[last];
_runtimeSelectables[last] = null;
_runtimeBaselineNavigation[last] = default(Navigation);
_runtimeSelectableCount--;
if (_lastSelected == selectable)
{
_lastSelected = null;
}
MarkRuntimeStateDirty();
return true;
}
return false;
}
public void InvalidateSelectableCache()
{
CaptureAllBaselines();
MarkRuntimeStateDirty();
}
public void OnSelect(BaseEventData eventData)
{
GameObject selectedObject = eventData != null ? eventData.selectedObject : null;
UXNavigationRuntime.NotifySelection(selectedObject);
}
internal void InvalidateSkipCacheOnly()
{
_isSkippedCacheValid = false;
}
internal int GetHierarchyDepth()
{
2026-04-09 15:21:49 +08:00
if (_cachedHierarchyDepth >= 0)
{
return _cachedHierarchyDepth;
}
int depth = 0;
Transform current = transform;
while (current != null)
{
depth++;
current = current.parent;
}
2026-04-09 15:21:49 +08:00
_cachedHierarchyDepth = depth;
return _cachedHierarchyDepth;
}
internal bool Owns(GameObject target)
{
if (target == null)
{
return false;
}
UXNavigationScope nearestScope = target.GetComponentInParent<UXNavigationScope>(true);
return nearestScope == this;
}
internal Selectable GetPreferredSelectable()
{
if (_rememberLastSelection && IsSelectableValid(_lastSelected))
{
return _lastSelected;
}
if (IsSelectableValid(_defaultSelectable))
{
return _defaultSelectable;
}
if (!_autoSelectFirstAvailable)
{
return null;
}
Selectable selectable = FirstUsable(_bakedSelectables, BakedSelectableCount);
if (selectable != null)
{
return selectable;
}
return FirstUsable(_runtimeSelectables, _runtimeSelectableCount);
}
2026-03-20 19:40:12 +08:00
internal bool HasAvailableSelectable()
{
return IsSelectableValid(_defaultSelectable)
|| FirstUsable(_bakedSelectables, BakedSelectableCount) != null
|| FirstUsable(_runtimeSelectables, _runtimeSelectableCount) != null;
2026-03-20 19:40:12 +08:00
}
internal void RecordSelection(GameObject selectedObject)
{
if (!_rememberLastSelection || selectedObject == null)
{
return;
}
2026-04-09 15:21:49 +08:00
Selectable selectable = GetSelectableFromObject(selectedObject);
if (IsSelectableValid(selectable))
{
_lastSelected = selectable;
}
}
internal bool IsSelectableOwnedAndValid(GameObject selectedObject)
{
2026-04-09 15:21:49 +08:00
return IsSelectableValid(GetSelectableFromObject(selectedObject));
}
internal void SetNavigationSuppressed(bool suppressed)
{
if (_navigationSuppressed == suppressed)
{
return;
}
CaptureAllBaselines();
_navigationSuppressed = suppressed;
ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed);
}
private void EnsureRuntimeBuffers()
{
int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0;
if (_runtimeSelectables == null || _runtimeSelectables.Length != capacity)
{
_runtimeSelectables = capacity > 0 ? new Selectable[capacity] : System.Array.Empty<Selectable>();
_runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty<Navigation>();
_runtimeSelectableCount = 0;
}
int bakedCount = BakedSelectableCount;
if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount)
{
_bakedBaselineNavigation = bakedCount > 0 ? new Navigation[bakedCount] : System.Array.Empty<Navigation>();
}
}
private void CaptureAllBaselines()
{
EnsureRuntimeBuffers();
CaptureBaseline(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount);
CaptureBaseline(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount);
}
private static void CaptureBaseline(Selectable[] selectables, Navigation[] baseline, int count)
{
if (selectables == null || baseline == null)
{
return;
}
for (int i = 0; i < count; i++)
{
Selectable selectable = selectables[i];
if (selectable != null)
{
baseline[i] = selectable.navigation;
}
}
}
private static void ApplySuppression(Selectable[] selectables, Navigation[] baseline, int count, bool suppressed)
{
if (selectables == null || baseline == null)
{
return;
}
for (int i = 0; i < count; i++)
{
Selectable selectable = selectables[i];
if (selectable == null)
{
continue;
}
if (suppressed)
{
SetSelectableSuppressed(selectable, true);
}
else
{
selectable.navigation = baseline[i];
}
}
}
private static void SetSelectableSuppressed(Selectable selectable, bool suppressed)
{
if (selectable == null || !suppressed)
{
return;
}
2026-04-09 15:21:49 +08:00
Navigation navigation = selectable.navigation;
navigation.mode = Navigation.Mode.None;
selectable.navigation = navigation;
}
private bool ContainsSelectable(Selectable selectable)
{
return IndexOf(_bakedSelectables, BakedSelectableCount, selectable) >= 0
|| IndexOf(_runtimeSelectables, _runtimeSelectableCount, selectable) >= 0;
}
private bool IsSelectableValid(Selectable selectable)
{
return IsSelectableUsable(selectable) && ContainsSelectable(selectable);
}
private static Selectable FirstUsable(Selectable[] selectables, int count)
{
if (selectables == null)
{
return null;
}
for (int i = 0; i < count; i++)
{
Selectable selectable = selectables[i];
if (IsSelectableUsable(selectable))
{
return selectable;
}
}
return null;
}
private static int IndexOf(Selectable[] selectables, int count, Selectable selectable)
2026-04-09 15:21:49 +08:00
{
if (selectables == null || selectable == null)
{
return InvalidIndex;
}
for (int i = 0; i < count; i++)
{
if (selectables[i] == selectable)
{
return i;
}
}
return InvalidIndex;
2026-04-09 15:21:49 +08:00
}
private static bool IsSelectableUsable(Selectable selectable)
{
return selectable != null && selectable.IsActive() && selectable.IsInteractable();
2026-04-09 15:21:49 +08:00
}
private static Selectable GetSelectableFromObject(GameObject selectedObject)
{
if (selectedObject == null)
{
return null;
}
return selectedObject.TryGetComponent(out Selectable selectable)
? selectable
: selectedObject.GetComponentInParent<Selectable>();
}
private void MarkRuntimeStateDirty()
{
if (UXNavigationRuntime.TryGetInstance(out var runtime))
{
runtime.MarkStateDirty();
}
}
private static void ReportCapacityExceeded()
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
Debug.LogError("UXNavigationScope runtime selectable capacity exceeded.");
#endif
}
}
}
#endif