增加UXNavigation 用于适配手柄导航 适配UI框架 Scope架构
This commit is contained in:
parent
4364f4673d
commit
756be161fd
@ -477,7 +477,7 @@ namespace UnityEngine.UI
|
||||
|
||||
foreach (var scope in _scopes.Values)
|
||||
{
|
||||
if (!IsScopeActive(scope))
|
||||
if (!IsScopeActive(scope) || !UXNavigationRuntime.IsHolderWithinTopScope(scope.Holder))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -492,7 +492,9 @@ namespace UnityEngine.UI
|
||||
|
||||
foreach (var scope in _scopes.Values)
|
||||
{
|
||||
if (IsScopeActive(scope) && !_ancestorHolders.Contains(scope.Holder))
|
||||
if (IsScopeActive(scope)
|
||||
&& UXNavigationRuntime.IsHolderWithinTopScope(scope.Holder)
|
||||
&& !_ancestorHolders.Contains(scope.Holder))
|
||||
{
|
||||
_leafScopes.Add(scope);
|
||||
}
|
||||
|
||||
8
Runtime/UXComponent/Navigation.meta
Normal file
8
Runtime/UXComponent/Navigation.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 454112a9b68b5d140aa6ba4085af80a8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
10
Runtime/UXComponent/Navigation/UXInputMode.cs
Normal file
10
Runtime/UXComponent/Navigation/UXInputMode.cs
Normal file
@ -0,0 +1,10 @@
|
||||
#if INPUTSYSTEM_SUPPORT
|
||||
namespace UnityEngine.UI
|
||||
{
|
||||
public enum UXInputMode : byte
|
||||
{
|
||||
Pointer = 0,
|
||||
Gamepad = 1
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Runtime/UXComponent/Navigation/UXInputMode.cs.meta
Normal file
11
Runtime/UXComponent/Navigation/UXInputMode.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b1d6174ce87303c419a2ca88b7d1fcc8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
169
Runtime/UXComponent/Navigation/UXInputModeService.cs
Normal file
169
Runtime/UXComponent/Navigation/UXInputModeService.cs
Normal file
@ -0,0 +1,169 @@
|
||||
#if INPUTSYSTEM_SUPPORT
|
||||
using System;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.InputSystem.Controls;
|
||||
|
||||
namespace UnityEngine.UI
|
||||
{
|
||||
internal sealed class UXInputModeService : MonoBehaviour
|
||||
{
|
||||
private static UXInputModeService _instance;
|
||||
|
||||
private InputAction _pointerAction;
|
||||
private InputAction _gamepadAction;
|
||||
|
||||
public static UXInputMode CurrentMode { get; private set; } = UXInputMode.Pointer;
|
||||
|
||||
public static event Action<UXInputMode> OnModeChanged;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
EnsureInstance();
|
||||
}
|
||||
|
||||
internal static UXInputModeService EnsureInstance()
|
||||
{
|
||||
if (_instance != null)
|
||||
{
|
||||
return _instance;
|
||||
}
|
||||
|
||||
var go = new GameObject("[UXInputModeService]");
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
DontDestroyOnLoad(go);
|
||||
_instance = go.AddComponent<UXInputModeService>();
|
||||
return _instance;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
_instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
hideFlags = HideFlags.HideAndDontSave;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
CreateActions();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
DisposeActions();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
DisposeActions();
|
||||
if (_instance == this)
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateActions()
|
||||
{
|
||||
if (_pointerAction != null || _gamepadAction != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pointerAction = new InputAction("UXPointerInput", InputActionType.PassThrough);
|
||||
_pointerAction.AddBinding("<Keyboard>/anyKey");
|
||||
_pointerAction.AddBinding("<Mouse>/delta");
|
||||
_pointerAction.AddBinding("<Mouse>/scroll");
|
||||
_pointerAction.AddBinding("<Mouse>/leftButton");
|
||||
_pointerAction.AddBinding("<Mouse>/rightButton");
|
||||
_pointerAction.AddBinding("<Mouse>/middleButton");
|
||||
_pointerAction.performed += OnPointerInput;
|
||||
_pointerAction.Enable();
|
||||
|
||||
_gamepadAction = new InputAction("UXGamepadInput", InputActionType.PassThrough);
|
||||
_gamepadAction.AddBinding("<Gamepad>/*");
|
||||
_gamepadAction.AddBinding("<Joystick>/*");
|
||||
_gamepadAction.performed += OnGamepadInput;
|
||||
_gamepadAction.Enable();
|
||||
}
|
||||
|
||||
private void DisposeActions()
|
||||
{
|
||||
if (_pointerAction != null)
|
||||
{
|
||||
_pointerAction.performed -= OnPointerInput;
|
||||
_pointerAction.Disable();
|
||||
_pointerAction.Dispose();
|
||||
_pointerAction = null;
|
||||
}
|
||||
|
||||
if (_gamepadAction != null)
|
||||
{
|
||||
_gamepadAction.performed -= OnGamepadInput;
|
||||
_gamepadAction.Disable();
|
||||
_gamepadAction.Dispose();
|
||||
_gamepadAction = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnPointerInput(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!IsInputMeaningful(context.control))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetMode(UXInputMode.Pointer);
|
||||
}
|
||||
|
||||
private static void OnGamepadInput(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!IsInputMeaningful(context.control))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetMode(UXInputMode.Gamepad);
|
||||
}
|
||||
|
||||
private static bool IsInputMeaningful(InputControl control)
|
||||
{
|
||||
if (control == null || control.device == null || control.synthetic)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (control)
|
||||
{
|
||||
case ButtonControl button:
|
||||
return button.IsPressed();
|
||||
case StickControl stick:
|
||||
return stick.ReadValue().sqrMagnitude >= 0.04f;
|
||||
case Vector2Control vector2:
|
||||
return vector2.ReadValue().sqrMagnitude >= 0.04f;
|
||||
case AxisControl axis:
|
||||
return Mathf.Abs(axis.ReadValue()) >= 0.2f;
|
||||
default:
|
||||
return !control.noisy;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetMode(UXInputMode mode)
|
||||
{
|
||||
EnsureInstance();
|
||||
if (CurrentMode == mode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentMode = mode;
|
||||
OnModeChanged?.Invoke(mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Runtime/UXComponent/Navigation/UXInputModeService.cs.meta
Normal file
11
Runtime/UXComponent/Navigation/UXInputModeService.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca8818b35acd2324198177d2fd3f359b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs
Normal file
20
Runtime/UXComponent/Navigation/UXNavigationLayerWatcher.cs
Normal file
@ -0,0 +1,20 @@
|
||||
#if INPUTSYSTEM_SUPPORT
|
||||
namespace UnityEngine.UI
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
internal sealed class UXNavigationLayerWatcher : MonoBehaviour
|
||||
{
|
||||
private UXNavigationRuntime _runtime;
|
||||
|
||||
internal void Initialize(UXNavigationRuntime runtime)
|
||||
{
|
||||
_runtime = runtime;
|
||||
}
|
||||
|
||||
private void OnTransformChildrenChanged()
|
||||
{
|
||||
_runtime?.MarkDiscoveryDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0268df3dd46bb194fa4ae7ec48be7702
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
455
Runtime/UXComponent/Navigation/UXNavigationRuntime.cs
Normal file
455
Runtime/UXComponent/Navigation/UXNavigationRuntime.cs
Normal file
@ -0,0 +1,455 @@
|
||||
#if INPUTSYSTEM_SUPPORT
|
||||
using System.Collections.Generic;
|
||||
using AlicizaX;
|
||||
using AlicizaX.UI.Runtime;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace UnityEngine.UI
|
||||
{
|
||||
internal sealed class UXNavigationRuntime : MonoBehaviour
|
||||
{
|
||||
private static UXNavigationRuntime _instance;
|
||||
|
||||
private const float DiscoveryInterval = 0.5f;
|
||||
private readonly HashSet<UXNavigationScope> _scopeSet = new();
|
||||
private readonly List<UXNavigationScope> _scopes = new(32);
|
||||
private readonly HashSet<Transform> _interactiveLayerRoots = new();
|
||||
|
||||
private Transform _uiCanvasRoot;
|
||||
private UXNavigationScope _topScope;
|
||||
private GameObject _lastObservedSelection;
|
||||
private float _nextDiscoveryTime;
|
||||
private bool _discoveryDirty = true;
|
||||
private ulong _activationSerial;
|
||||
private bool _missingEventSystemLogged;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
EnsureInstance();
|
||||
UXInputModeService.EnsureInstance();
|
||||
}
|
||||
|
||||
internal static UXNavigationRuntime EnsureInstance()
|
||||
{
|
||||
if (_instance != null)
|
||||
{
|
||||
return _instance;
|
||||
}
|
||||
|
||||
var go = new GameObject("[UXNavigationRuntime]");
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
DontDestroyOnLoad(go);
|
||||
_instance = go.AddComponent<UXNavigationRuntime>();
|
||||
return _instance;
|
||||
}
|
||||
|
||||
internal static bool TryGetInstance(out UXNavigationRuntime runtime)
|
||||
{
|
||||
runtime = _instance;
|
||||
return runtime != null;
|
||||
}
|
||||
|
||||
internal static bool IsHolderWithinTopScope(UIHolderObjectBase holder)
|
||||
{
|
||||
if (_instance == null || _instance._topScope == null || holder == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _instance.IsHolderOwnedByScope(holder, _instance._topScope);
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_instance != null && _instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
_instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
hideFlags = HideFlags.HideAndDontSave;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
UXInputModeService.OnModeChanged += OnInputModeChanged;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UXInputModeService.OnModeChanged -= OnInputModeChanged;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
UXInputModeService.OnModeChanged -= OnInputModeChanged;
|
||||
if (_instance == this)
|
||||
{
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal void RegisterScope(UXNavigationScope scope)
|
||||
{
|
||||
if (scope == null || !_scopeSet.Add(scope))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scopes.Add(scope);
|
||||
MarkDiscoveryDirty();
|
||||
}
|
||||
|
||||
internal void UnregisterScope(UXNavigationScope scope)
|
||||
{
|
||||
if (scope == null || !_scopeSet.Remove(scope))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_topScope == scope)
|
||||
{
|
||||
_topScope = null;
|
||||
}
|
||||
|
||||
scope.SetNavigationSuppressed(false);
|
||||
_scopes.Remove(scope);
|
||||
MarkDiscoveryDirty();
|
||||
}
|
||||
|
||||
internal void MarkDiscoveryDirty()
|
||||
{
|
||||
_discoveryDirty = true;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
TryBindUIRoot();
|
||||
if (_uiCanvasRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_discoveryDirty || Time.unscaledTime >= _nextDiscoveryTime)
|
||||
{
|
||||
DiscoverScopes();
|
||||
}
|
||||
|
||||
UXNavigationScope newTopScope = FindTopScope();
|
||||
if (!ReferenceEquals(_topScope, newTopScope))
|
||||
{
|
||||
_topScope = newTopScope;
|
||||
}
|
||||
|
||||
ApplyScopeSuppression();
|
||||
|
||||
if (UXInputModeService.CurrentMode == UXInputMode.Gamepad)
|
||||
{
|
||||
EnsureGamepadSelection();
|
||||
if (_topScope != null)
|
||||
{
|
||||
Cursor.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
TrackSelection();
|
||||
}
|
||||
|
||||
private void TryBindUIRoot()
|
||||
{
|
||||
if (_uiCanvasRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IUIModule uiModule = ModuleSystem.GetModule<IUIModule>();
|
||||
if (uiModule?.UICanvasRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_uiCanvasRoot = uiModule.UICanvasRoot;
|
||||
CacheInteractiveLayers();
|
||||
DiscoverScopes();
|
||||
}
|
||||
|
||||
private void CacheInteractiveLayers()
|
||||
{
|
||||
_interactiveLayerRoots.Clear();
|
||||
if (_uiCanvasRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string cacheLayerName = $"Layer{(int)UILayer.All}-{UILayer.All}";
|
||||
for (int i = 0; i < _uiCanvasRoot.childCount; i++)
|
||||
{
|
||||
Transform child = _uiCanvasRoot.GetChild(i);
|
||||
if (child == null || child.name == cacheLayerName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_interactiveLayerRoots.Add(child);
|
||||
if (child.GetComponent<UXNavigationLayerWatcher>() == null)
|
||||
{
|
||||
var watcher = child.gameObject.AddComponent<UXNavigationLayerWatcher>();
|
||||
watcher.Initialize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DiscoverScopes()
|
||||
{
|
||||
_discoveryDirty = false;
|
||||
_nextDiscoveryTime = Time.unscaledTime + DiscoveryInterval;
|
||||
CacheInteractiveLayers();
|
||||
bool addedScope = false;
|
||||
|
||||
foreach (Transform layerRoot in _interactiveLayerRoots)
|
||||
{
|
||||
if (layerRoot == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
UIHolderObjectBase[] holders = layerRoot.GetComponentsInChildren<UIHolderObjectBase>(true);
|
||||
for (int i = 0; i < holders.Length; i++)
|
||||
{
|
||||
UIHolderObjectBase holder = holders[i];
|
||||
if (holder == null || holder.GetComponent<UXNavigationScope>() != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
holder.gameObject.AddComponent<UXNavigationScope>();
|
||||
addedScope = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedScope)
|
||||
{
|
||||
for (int i = 0; i < _scopes.Count; i++)
|
||||
{
|
||||
_scopes[i]?.InvalidateSelectableCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UXNavigationScope FindTopScope()
|
||||
{
|
||||
UXNavigationScope bestScope = null;
|
||||
for (int i = 0; i < _scopes.Count; i++)
|
||||
{
|
||||
UXNavigationScope scope = _scopes[i];
|
||||
if (scope == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool available = IsScopeAvailable(scope);
|
||||
if (scope.WasAvailable != available)
|
||||
{
|
||||
scope.WasAvailable = available;
|
||||
if (available)
|
||||
{
|
||||
scope.ActivationSerial = ++_activationSerial;
|
||||
}
|
||||
}
|
||||
|
||||
if (!available)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bestScope == null || CompareScopePriority(scope, bestScope) < 0)
|
||||
{
|
||||
bestScope = scope;
|
||||
}
|
||||
}
|
||||
|
||||
return bestScope;
|
||||
}
|
||||
|
||||
private bool IsScopeAvailable(UXNavigationScope scope)
|
||||
{
|
||||
if (scope == null || !scope.isActiveAndEnabled || !scope.gameObject.activeInHierarchy)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Canvas canvas = scope.Canvas;
|
||||
if (canvas == null || canvas.gameObject.layer != UIComponent.UIShowLayer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryGetInteractiveLayerRoot(scope.transform, out _);
|
||||
}
|
||||
|
||||
private bool TryGetInteractiveLayerRoot(Transform current, out Transform layerRoot)
|
||||
{
|
||||
layerRoot = null;
|
||||
if (current == null || _uiCanvasRoot == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
while (current != null)
|
||||
{
|
||||
if (current.parent == _uiCanvasRoot)
|
||||
{
|
||||
layerRoot = current;
|
||||
return _interactiveLayerRoots.Contains(current);
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool IsHolderOwnedByScope(UIHolderObjectBase holder, UXNavigationScope scope)
|
||||
{
|
||||
if (holder == null || scope == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UXNavigationScope nearestScope = holder.GetComponent<UXNavigationScope>();
|
||||
if (nearestScope == null)
|
||||
{
|
||||
nearestScope = holder.GetComponentInParent<UXNavigationScope>(true);
|
||||
}
|
||||
|
||||
return nearestScope == scope;
|
||||
}
|
||||
|
||||
private void ApplyScopeSuppression()
|
||||
{
|
||||
for (int i = 0; i < _scopes.Count; i++)
|
||||
{
|
||||
UXNavigationScope scope = _scopes[i];
|
||||
if (scope == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool suppress = IsScopeAvailable(scope)
|
||||
&& _topScope != null
|
||||
&& !ReferenceEquals(scope, _topScope)
|
||||
&& _topScope.BlockLowerScopes
|
||||
&& CompareScopePriority(_topScope, scope) < 0;
|
||||
scope.SetNavigationSuppressed(suppress);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureGamepadSelection()
|
||||
{
|
||||
EventSystem eventSystem = EventSystem.current;
|
||||
if (eventSystem == null)
|
||||
{
|
||||
if (!_missingEventSystemLogged)
|
||||
{
|
||||
Debug.LogWarning("UXNavigationRuntime requires an active EventSystem for gamepad navigation.");
|
||||
_missingEventSystemLogged = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_missingEventSystemLogged = false;
|
||||
|
||||
if (_topScope == null || !_topScope.RequireSelectionWhenGamepad)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject currentSelected = eventSystem.currentSelectedGameObject;
|
||||
if (_topScope.IsSelectableOwnedAndValid(currentSelected))
|
||||
{
|
||||
_topScope.RecordSelection(currentSelected);
|
||||
return;
|
||||
}
|
||||
|
||||
Selectable preferred = _topScope.GetPreferredSelectable();
|
||||
eventSystem.SetSelectedGameObject(preferred != null ? preferred.gameObject : null);
|
||||
_lastObservedSelection = eventSystem.currentSelectedGameObject;
|
||||
if (_lastObservedSelection != null)
|
||||
{
|
||||
_topScope.RecordSelection(_lastObservedSelection);
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackSelection()
|
||||
{
|
||||
EventSystem eventSystem = EventSystem.current;
|
||||
if (eventSystem == null)
|
||||
{
|
||||
_lastObservedSelection = null;
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject currentSelected = eventSystem.currentSelectedGameObject;
|
||||
if (ReferenceEquals(_lastObservedSelection, currentSelected))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastObservedSelection = currentSelected;
|
||||
if (_topScope != null && _topScope.IsSelectableOwnedAndValid(currentSelected))
|
||||
{
|
||||
_topScope.RecordSelection(currentSelected);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInputModeChanged(UXInputMode mode)
|
||||
{
|
||||
EventSystem eventSystem = EventSystem.current;
|
||||
if (mode == UXInputMode.Pointer)
|
||||
{
|
||||
if (_topScope != null)
|
||||
{
|
||||
Cursor.visible = true;
|
||||
if (eventSystem != null)
|
||||
{
|
||||
eventSystem.SetSelectedGameObject(null);
|
||||
}
|
||||
}
|
||||
|
||||
_lastObservedSelection = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_topScope != null)
|
||||
{
|
||||
Cursor.visible = false;
|
||||
}
|
||||
|
||||
EnsureGamepadSelection();
|
||||
}
|
||||
|
||||
private static int CompareScopePriority(UXNavigationScope left, UXNavigationScope right)
|
||||
{
|
||||
int leftOrder = left.Canvas != null ? left.Canvas.sortingOrder : int.MinValue;
|
||||
int rightOrder = right.Canvas != null ? right.Canvas.sortingOrder : int.MinValue;
|
||||
int orderCompare = rightOrder.CompareTo(leftOrder);
|
||||
if (orderCompare != 0)
|
||||
{
|
||||
return orderCompare;
|
||||
}
|
||||
|
||||
int hierarchyCompare = right.GetHierarchyDepth().CompareTo(left.GetHierarchyDepth());
|
||||
if (hierarchyCompare != 0)
|
||||
{
|
||||
return hierarchyCompare;
|
||||
}
|
||||
|
||||
return right.ActivationSerial.CompareTo(left.ActivationSerial);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
11
Runtime/UXComponent/Navigation/UXNavigationRuntime.cs.meta
Normal file
11
Runtime/UXComponent/Navigation/UXNavigationRuntime.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d755cbf1369cfc4288d7b1261c523bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
302
Runtime/UXComponent/Navigation/UXNavigationScope.cs
Normal file
302
Runtime/UXComponent/Navigation/UXNavigationScope.cs
Normal file
@ -0,0 +1,302 @@
|
||||
#if INPUTSYSTEM_SUPPORT
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
11
Runtime/UXComponent/Navigation/UXNavigationScope.cs.meta
Normal file
11
Runtime/UXComponent/Navigation/UXNavigationScope.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85e4995cea5647b46995bb92b329207d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in New Issue
Block a user