487 lines
15 KiB
C#
487 lines
15 KiB
C#
#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;
|
|
private int _cachedHierarchyDepth = -1;
|
|
private bool _cachedIsSkipped;
|
|
private bool _isSkippedCacheValid;
|
|
private int _runtimeSelectableCount;
|
|
|
|
internal int RuntimeIndex { get; set; } = InvalidIndex;
|
|
internal ulong ActivationSerial { get; set; }
|
|
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;
|
|
|
|
internal bool IsNavigationSkipped
|
|
{
|
|
get
|
|
{
|
|
if (!_isSkippedCacheValid)
|
|
{
|
|
_cachedIsSkipped = GetComponentInParent<UXNavigationSkip>(true) != null;
|
|
_isSkippedCacheValid = true;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
private void OnTransformParentChanged()
|
|
{
|
|
_cachedCanvas = null;
|
|
_cachedHolder = null;
|
|
_cachedHierarchyDepth = -1;
|
|
_isSkippedCacheValid = false;
|
|
MarkRuntimeStateDirty();
|
|
}
|
|
|
|
#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()
|
|
{
|
|
if (_cachedHierarchyDepth >= 0)
|
|
{
|
|
return _cachedHierarchyDepth;
|
|
}
|
|
|
|
int depth = 0;
|
|
Transform current = transform;
|
|
while (current != null)
|
|
{
|
|
depth++;
|
|
current = current.parent;
|
|
}
|
|
|
|
_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);
|
|
}
|
|
|
|
internal bool HasAvailableSelectable()
|
|
{
|
|
return IsSelectableValid(_defaultSelectable)
|
|
|| FirstUsable(_bakedSelectables, BakedSelectableCount) != null
|
|
|| FirstUsable(_runtimeSelectables, _runtimeSelectableCount) != null;
|
|
}
|
|
|
|
internal void RecordSelection(GameObject selectedObject)
|
|
{
|
|
if (!_rememberLastSelection || selectedObject == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Selectable selectable = GetSelectableFromObject(selectedObject);
|
|
if (IsSelectableValid(selectable))
|
|
{
|
|
_lastSelected = selectable;
|
|
}
|
|
}
|
|
|
|
internal bool IsSelectableOwnedAndValid(GameObject selectedObject)
|
|
{
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (selectables == null || selectable == null)
|
|
{
|
|
return InvalidIndex;
|
|
}
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
if (selectables[i] == selectable)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return InvalidIndex;
|
|
}
|
|
|
|
private static bool IsSelectableUsable(Selectable selectable)
|
|
{
|
|
return selectable != null && selectable.IsActive() && selectable.IsInteractable();
|
|
}
|
|
|
|
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
|