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

755 lines
24 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;
private int[] _bakedSelectableHashIds = System.Array.Empty<int>();
private int[] _bakedSelectableHashIndices = System.Array.Empty<int>();
private int[] _runtimeSelectableHashIds = System.Array.Empty<int>();
private int[] _runtimeSelectableHashIndices = System.Array.Empty<int>();
private bool _selectableSetDirty = true;
private bool _selectableAvailabilityDirty = true;
private int _availableSelectableCount;
private Selectable _firstAvailableSelectable;
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;
MarkSelectableAvailabilityDirty();
MarkRuntimeStateDirty();
}
}
public bool RememberLastSelection => _rememberLastSelection;
public bool RequireSelectionWhenGamepad => _requireSelectionWhenGamepad;
public bool BlockLowerScopes => _blockLowerScopes;
internal bool IsNavigationSkipped
{
get
{
if (!_isSkippedCacheValid)
{
_cachedIsSkipped = HasSkipInParents();
_isSkippedCacheValid = true;
}
return _cachedIsSkipped;
}
}
private bool HasSkipInParents()
{
Transform current = transform;
while (current != null)
{
if (current.TryGetComponent(out UXNavigationSkip skip))
{
return true;
}
current = current.parent;
}
return false;
}
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(false);
RefreshBaselineWhenUnsuppressed();
}
private void OnEnable()
{
EnsureRuntimeBuffers(false);
RefreshBaselineWhenUnsuppressed();
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()
{
MarkSelectableAvailabilityDirty();
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(true);
if (_runtimeSelectableCount >= _runtimeSelectables.Length)
{
ReportCapacityExceeded();
return false;
}
int index = _runtimeSelectableCount++;
_runtimeSelectables[index] = selectable;
_runtimeBaselineNavigation[index] = selectable.navigation;
MarkSelectableSetDirty();
if (_navigationSuppressed)
{
SetSelectableSuppressed(selectable);
}
MarkRuntimeStateDirty();
return true;
}
public bool UnregisterSelectable(Selectable selectable)
{
if (selectable == null || _runtimeSelectables == null)
{
return false;
}
int index = FindRuntimeIndex(selectable);
if (index < 0)
{
return false;
}
if (_navigationSuppressed)
{
selectable.navigation = _runtimeBaselineNavigation[index];
}
int last = --_runtimeSelectableCount;
Selectable movedSelectable = _runtimeSelectables[last];
Navigation movedNavigation = _runtimeBaselineNavigation[last];
_runtimeSelectables[last] = null;
_runtimeBaselineNavigation[last] = default(Navigation);
if (index != last)
{
_runtimeSelectables[index] = movedSelectable;
_runtimeBaselineNavigation[index] = movedNavigation;
}
if (_lastSelected == selectable)
{
_lastSelected = null;
}
MarkSelectableSetDirty();
MarkRuntimeStateDirty();
return true;
}
public void InvalidateSelectableCache()
{
if (_navigationSuppressed)
{
ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, false);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, false);
CaptureBaselineBeforeSuppress();
ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, true);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, true);
}
else
{
RefreshBaselineWhenUnsuppressed();
}
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()
{
RefreshSelectableAvailabilityIfDirty();
if (_rememberLastSelection && IsSelectableValid(_lastSelected))
{
return _lastSelected;
}
if (IsSelectableValid(_defaultSelectable))
{
return _defaultSelectable;
}
return _autoSelectFirstAvailable ? _firstAvailableSelectable : null;
}
internal bool HasAvailableSelectable()
{
RefreshSelectableAvailabilityIfDirty();
if (IsSelectableValid(_defaultSelectable))
{
return true;
}
return _autoSelectFirstAvailable && _availableSelectableCount > 0;
}
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;
}
if (suppressed)
{
CaptureBaselineBeforeSuppress();
}
_navigationSuppressed = suppressed;
ApplySuppression(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount, suppressed);
ApplySuppression(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount, suppressed);
}
private void EnsureRuntimeBuffers(bool preserveRuntimeSelectables)
{
int capacity = _runtimeSelectableCapacity > 0 ? _runtimeSelectableCapacity : 0;
if (_runtimeSelectables == null || _runtimeSelectables.Length != capacity)
{
Selectable[] previousSelectables = _runtimeSelectables;
Navigation[] previousBaseline = _runtimeBaselineNavigation;
int previousCount = _runtimeSelectableCount;
_runtimeSelectables = capacity > 0 ? new Selectable[capacity] : System.Array.Empty<Selectable>();
_runtimeBaselineNavigation = capacity > 0 ? new Navigation[capacity] : System.Array.Empty<Navigation>();
CreateRuntimeHash(capacity);
_runtimeSelectableCount = 0;
if (preserveRuntimeSelectables && previousSelectables != null && capacity > 0)
{
int copyCount = previousCount < capacity ? previousCount : capacity;
for (int i = 0; i < copyCount; i++)
{
Selectable selectable = previousSelectables[i];
if (selectable == null)
{
continue;
}
_runtimeSelectables[_runtimeSelectableCount] = selectable;
_runtimeBaselineNavigation[_runtimeSelectableCount] = previousBaseline != null && i < previousBaseline.Length
? previousBaseline[i]
: selectable.navigation;
_runtimeSelectableCount++;
}
}
MarkSelectableSetDirty();
}
int bakedCount = BakedSelectableCount;
if (_bakedBaselineNavigation == null || _bakedBaselineNavigation.Length != bakedCount)
{
_bakedBaselineNavigation = bakedCount > 0 ? new Navigation[bakedCount] : System.Array.Empty<Navigation>();
CreateBakedHash(bakedCount);
MarkSelectableSetDirty();
}
}
private void CaptureBaselineBeforeSuppress()
{
EnsureRuntimeBuffers(true);
RefreshSelectableHashesIfDirty();
CaptureBaseline(_bakedSelectables, _bakedBaselineNavigation, BakedSelectableCount);
CaptureBaseline(_runtimeSelectables, _runtimeBaselineNavigation, _runtimeSelectableCount);
}
private void RefreshBaselineWhenUnsuppressed()
{
if (_navigationSuppressed)
{
return;
}
CaptureBaselineBeforeSuppress();
}
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);
}
else
{
selectable.navigation = baseline[i];
}
}
}
private static void SetSelectableSuppressed(Selectable selectable)
{
if (selectable == null)
{
return;
}
Navigation navigation = selectable.navigation;
navigation.mode = Navigation.Mode.None;
selectable.navigation = navigation;
}
private bool ContainsSelectable(Selectable selectable)
{
if (selectable == null)
{
return false;
}
RefreshSelectableHashesIfDirty();
int instanceId = selectable.GetInstanceID();
return FindHashIndex(_bakedSelectableHashIds, _bakedSelectableHashIndices, instanceId) >= 0
|| FindHashIndex(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, instanceId) >= 0;
}
private bool IsSelectableValid(Selectable selectable)
{
return IsSelectableUsable(selectable) && ContainsSelectable(selectable);
}
private void MarkSelectableSetDirty()
{
_selectableSetDirty = true;
MarkSelectableAvailabilityDirty();
}
private void MarkSelectableAvailabilityDirty()
{
_selectableAvailabilityDirty = true;
}
public void NotifySelectableStateChanged()
{
MarkSelectableAvailabilityDirty();
MarkRuntimeStateDirty();
}
private void RefreshSelectableAvailabilityIfDirty()
{
if (!_selectableAvailabilityDirty)
{
return;
}
_availableSelectableCount = 0;
_firstAvailableSelectable = null;
AccumulateAvailableSelectables(_bakedSelectables, BakedSelectableCount);
AccumulateAvailableSelectables(_runtimeSelectables, _runtimeSelectableCount);
_selectableAvailabilityDirty = false;
}
private void AccumulateAvailableSelectables(Selectable[] selectables, int count)
{
if (selectables == null)
{
return;
}
for (int i = 0; i < count; i++)
{
Selectable selectable = selectables[i];
if (!IsSelectableUsable(selectable))
{
continue;
}
if (_firstAvailableSelectable == null)
{
_firstAvailableSelectable = selectable;
}
_availableSelectableCount++;
}
}
private void RefreshSelectableHashesIfDirty()
{
if (!_selectableSetDirty)
{
return;
}
RebuildBakedHash();
RebuildRuntimeHash();
_selectableSetDirty = false;
}
private void RebuildBakedHash()
{
ClearHash(_bakedSelectableHashIds, _bakedSelectableHashIndices);
for (int i = 0; i < BakedSelectableCount; i++)
{
Selectable selectable = _bakedSelectables[i];
if (selectable != null)
{
AddHash(_bakedSelectableHashIds, _bakedSelectableHashIndices, selectable.GetInstanceID(), i);
}
}
}
private void RebuildRuntimeHash()
{
ClearHash(_runtimeSelectableHashIds, _runtimeSelectableHashIndices);
for (int i = 0; i < _runtimeSelectableCount; i++)
{
Selectable selectable = _runtimeSelectables[i];
if (selectable != null)
{
AddHash(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, selectable.GetInstanceID(), i);
}
}
}
private void CreateBakedHash(int itemCapacity)
{
int hashCapacity = GetHashCapacity(itemCapacity);
_bakedSelectableHashIds = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty<int>();
_bakedSelectableHashIndices = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty<int>();
}
private void CreateRuntimeHash(int itemCapacity)
{
int hashCapacity = GetHashCapacity(itemCapacity);
_runtimeSelectableHashIds = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty<int>();
_runtimeSelectableHashIndices = hashCapacity > 0 ? new int[hashCapacity] : System.Array.Empty<int>();
}
private static int GetHashCapacity(int itemCapacity)
{
if (itemCapacity <= 0)
{
return 0;
}
int hashCapacity = 1;
int required = itemCapacity << 1;
while (hashCapacity < required)
{
hashCapacity <<= 1;
}
return hashCapacity;
}
private static void ClearHash(int[] ids, int[] indices)
{
if (ids == null || indices == null)
{
return;
}
for (int i = 0; i < ids.Length; i++)
{
ids[i] = 0;
indices[i] = InvalidIndex;
}
}
private static int FindHashIndex(int[] ids, int[] indices, int instanceId)
{
if (ids == null || indices == null || ids.Length == 0 || instanceId == 0)
{
return InvalidIndex;
}
int mask = ids.Length - 1;
int index = instanceId & mask;
for (int i = 0; i < ids.Length; i++)
{
int storedId = ids[index];
if (storedId == 0)
{
return InvalidIndex;
}
if (storedId == instanceId)
{
return indices[index];
}
index = (index + 1) & mask;
}
return InvalidIndex;
}
private static void AddHash(int[] ids, int[] indices, int instanceId, int selectableIndex)
{
if (ids == null || indices == null || ids.Length == 0 || instanceId == 0)
{
return;
}
int mask = ids.Length - 1;
int index = instanceId & mask;
for (int i = 0; i < ids.Length; i++)
{
int storedId = ids[index];
if (storedId == 0 || storedId == instanceId)
{
ids[index] = instanceId;
indices[index] = selectableIndex;
return;
}
index = (index + 1) & mask;
}
}
private int FindRuntimeIndex(Selectable selectable)
{
if (selectable == null)
{
return InvalidIndex;
}
RefreshSelectableHashesIfDirty();
return FindHashIndex(_runtimeSelectableHashIds, _runtimeSelectableHashIndices, selectable.GetInstanceID());
}
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