新增UXSelectable 。对UXButton组件的手柄支持扩展

This commit is contained in:
陈思海 2025-12-03 17:26:55 +08:00
parent dfef3f5199
commit 74aa459a38
12 changed files with 841 additions and 40 deletions

View File

@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AlicizaX.UI.Extension;
using AlicizaX.UI.Extension.Editor;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditorInternal;
using UnityEditor.UI;
using UnityEngine;
@ -19,6 +22,11 @@ internal class UXButtonEditor : Editor
Event
}
GUIContent m_VisualizeNavigation = EditorGUIUtility.TrTextContent("Visualize", "Show navigation flows between selectable UI elements.");
private static bool s_ShowNavigation = false;
private static string s_ShowNavigationKey = "SelectableEditor.ShowNavigation";
private static List<UXButtonEditor> s_Editors = new List<UXButtonEditor>();
private SerializedProperty m_Interactable;
private SerializedProperty m_Mode;
private SerializedProperty m_OnValueChanged;
@ -40,6 +48,8 @@ internal class UXButtonEditor : Editor
private SerializedProperty hoverAudioClip;
private SerializedProperty clickAudioClip;
private SerializedProperty m_Navigation;
private UXButton mTarget;
private void OnEnable()
@ -54,13 +64,103 @@ internal class UXButtonEditor : Editor
m_TransitionData = serializedObject.FindProperty("m_TransitionData");
m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions");
m_SelectionState = serializedObject.FindProperty("m_SelectionState");
m_Navigation = serializedObject.FindProperty("m_Navigation");
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
clickAudioClip = serializedObject.FindProperty("clickAudioClip");
CreateChildTransitionList();
s_Editors.Add(this);
RegisterStaticOnSceneGUI();
s_ShowNavigation = EditorPrefs.GetBool(s_ShowNavigationKey);
}
protected virtual void OnDisable()
{
s_Editors.Remove(this);
RegisterStaticOnSceneGUI();
}
#region Navigation
private void RegisterStaticOnSceneGUI()
{
SceneView.duringSceneGui -= StaticOnSceneGUI;
if (s_Editors.Count > 0)
SceneView.duringSceneGui += StaticOnSceneGUI;
}
private static void StaticOnSceneGUI(SceneView view)
{
if (!s_ShowNavigation)
return;
UXSelectable[] selectables = UXSelectable.allSelectablesArray;
for (int i = 0; i < selectables.Length; i++)
{
UXSelectable s = selectables[i];
if (StageUtility.IsGameObjectRenderedByCamera(s.gameObject, Camera.current))
DrawNavigationForSelectable(s);
}
}
private static void DrawNavigationForSelectable(UXSelectable sel)
{
if (sel == null)
return;
Transform transform = sel.transform;
bool active = Selection.transforms.Any(e => e == transform);
Handles.color = new Color(1.0f, 0.6f, 0.2f, active ? 1.0f : 0.4f);
DrawNavigationArrow(-Vector2.right, sel, sel.FindSelectableOnLeft());
DrawNavigationArrow(Vector2.up, sel, sel.FindSelectableOnUp());
Handles.color = new Color(1.0f, 0.9f, 0.1f, active ? 1.0f : 0.4f);
DrawNavigationArrow(Vector2.right, sel, sel.FindSelectableOnRight());
DrawNavigationArrow(-Vector2.up, sel, sel.FindSelectableOnDown());
}
const float kArrowThickness = 2.5f;
const float kArrowHeadSize = 1.2f;
private static void DrawNavigationArrow(Vector2 direction, UXSelectable fromObj, UXSelectable toObj)
{
if (fromObj == null || toObj == null)
return;
Transform fromTransform = fromObj.transform;
Transform toTransform = toObj.transform;
Vector2 sideDir = new Vector2(direction.y, -direction.x);
Vector3 fromPoint = fromTransform.TransformPoint(GetPointOnRectEdge(fromTransform as RectTransform, direction));
Vector3 toPoint = toTransform.TransformPoint(GetPointOnRectEdge(toTransform as RectTransform, -direction));
float fromSize = HandleUtility.GetHandleSize(fromPoint) * 0.05f;
float toSize = HandleUtility.GetHandleSize(toPoint) * 0.05f;
fromPoint += fromTransform.TransformDirection(sideDir) * fromSize;
toPoint += toTransform.TransformDirection(sideDir) * toSize;
float length = Vector3.Distance(fromPoint, toPoint);
Vector3 fromTangent = fromTransform.rotation * direction * length * 0.3f;
Vector3 toTangent = toTransform.rotation * -direction * length * 0.3f;
Handles.DrawBezier(fromPoint, toPoint, fromPoint + fromTangent, toPoint + toTangent, Handles.color, null, kArrowThickness);
Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction - sideDir) * toSize * kArrowHeadSize);
Handles.DrawAAPolyLine(kArrowThickness, toPoint, toPoint + toTransform.rotation * (-direction + sideDir) * toSize * kArrowHeadSize);
}
private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir)
{
if (rect == null)
return Vector3.zero;
if (dir != Vector2.zero)
dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y));
dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f);
return dir;
}
#endregion
private void CreateChildTransitionList()
{
m_ChildTransitionList = new ReorderableList(serializedObject, m_ChildTransitions, true, false, true, true);
@ -198,8 +298,23 @@ internal class UXButtonEditor : Editor
m_Mode.enumValueIndex = (int)modeType;
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.PropertyField(m_Navigation.FindPropertyRelative("m_Mode"),new GUIContent("Navigation"));
Rect toggleRect = EditorGUILayout.GetControlRect();
toggleRect.xMin += EditorGUIUtility.labelWidth;
s_ShowNavigation = GUI.Toggle(toggleRect, s_ShowNavigation, m_VisualizeNavigation, EditorStyles.miniButton);
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool(s_ShowNavigationKey, s_ShowNavigation);
SceneView.RepaintAll();
}
var interactable = GUILayoutHelper.DrawToggle(m_Interactable.boolValue, customSkin, "Interactable");
if (interactable != m_Interactable.boolValue)
{

View File

@ -2,7 +2,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using AlicizaX.UI.Extension;
using AlicizaX.UI.Extension.Utility;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
@ -37,9 +36,7 @@ internal enum SelectionState
[ExecuteInEditMode]
[DisallowMultipleComponent]
public class UXButton : UIBehaviour, IButton,
IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler,
IPointerExitHandler, IPointerClickHandler, ISubmitHandler
public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler
{
#region Serialized Fields
@ -65,6 +62,13 @@ public class UXButton : UIBehaviour, IButton,
private Coroutine _resetRoutine;
private WaitForSeconds _waitFadeDuration;
// 静态锁(用于 normal 模式点击后的“保持 Selected”并能转移
private static UXButton s_LockedButton = null;
private bool m_IsFocusLocked = false;
private bool m_IsNavFocused = false;
private static readonly Dictionary<string, int> _animTriggerCache = new()
{
{ "Normal", Animator.StringToHash("Normal") },
@ -112,9 +116,25 @@ public class UXButton : UIBehaviour, IButton,
if (_mTogSelected == value) return;
_mTogSelected = value;
m_OnValueChanged?.Invoke(value);
// ------------- 关键修复 -------------
// 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected无论逻辑是否为 selected
// 聚焦判定使用 m_IsNavFocusedOnSelect/OnDeselect 管理)或 EventSystem.current.currentSelectedGameObject == gameObject。
bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
bool isFocused = m_IsNavFocused || isEventSystemSelected;
if (m_Mode == ButtonModeType.Toggle && isFocused)
{
// 即使逻辑值为 false也保持 Selected 视觉,因为焦点还在该控件上
SetState(SelectionState.Selected);
}
else
{
// 否则按逻辑恢复 Selected / Normal
SetState(value ? SelectionState.Selected : SelectionState.Normal);
}
}
}
public Button.ButtonClickedEvent onClick
{
@ -146,43 +166,105 @@ public class UXButton : UIBehaviour, IButton,
base.OnDestroy();
}
protected override void OnSetProperty()
{
ApplyVisualState(m_SelectionState, true);
}
public override bool IsInteractable()
{
return base.IsInteractable() && m_Interactable;
}
#endregion
#region Static lock helper
private static void SetLockedButton(UXButton newLocked)
{
if (s_LockedButton == newLocked)
return;
if (s_LockedButton != null)
{
var old = s_LockedButton;
s_LockedButton = null;
old.m_IsFocusLocked = false;
if (old._mTogSelected && old.m_Mode == ButtonModeType.Toggle)
old.SetState(SelectionState.Selected);
else
old.SetState(SelectionState.Normal);
}
if (newLocked != null)
{
s_LockedButton = newLocked;
s_LockedButton.m_IsFocusLocked = true;
s_LockedButton.SetState(SelectionState.Selected);
}
}
#endregion
#region Pointer Handlers
public void OnPointerDown(PointerEventData eventData)
public override void OnPointerDown(PointerEventData eventData)
{
if (!CanProcess()) return;
m_IsDown = true;
m_HasExitedWhileDown = false;
SetState(SelectionState.Pressed);
}
public void OnPointerUp(PointerEventData eventData)
public override void OnPointerUp(PointerEventData eventData)
{
if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
return;
m_IsDown = false;
var newState = _mTogSelected
? SelectionState.Selected
: m_HasExitedWhileDown
? SelectionState.Normal
: SelectionState.Highlighted;
if (m_IsFocusLocked || (_mTogSelected && m_Mode == ButtonModeType.Toggle && navigation.mode != UXNavigation.Mode.None))
{
SetState(SelectionState.Selected);
return;
}
var newState = m_HasExitedWhileDown ? SelectionState.Normal : SelectionState.Highlighted;
SetState(newState);
}
public void OnPointerEnter(PointerEventData eventData)
public override void OnPointerEnter(PointerEventData eventData)
{
if (!CanProcess()) return;
if (!CanProcessEnter()) return;
m_HasExitedWhileDown = false;
// 如果 toggle 模式并且聚焦(导航/或 EventSystem 选中),保持 Selected不触发 Highlight
bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
bool isFocused = m_IsNavFocused || isEventSystemSelected;
if (m_Mode == ButtonModeType.Toggle && isFocused)
{
SetState(SelectionState.Selected);
return;
}
if (navigation.mode != UXNavigation.Mode.None)
{
if ((m_Mode == ButtonModeType.Normal && m_IsFocusLocked) ||
(m_Mode == ButtonModeType.Toggle && _mTogSelected))
{
SetState(SelectionState.Selected);
return;
}
}
if (m_IsDown) return;
SetState(SelectionState.Highlighted);
PlayAudio(hoverAudioClip);
}
public void OnPointerExit(PointerEventData eventData)
public override void OnPointerExit(PointerEventData eventData)
{
if (!m_Interactable) return;
if (m_IsDown)
@ -191,6 +273,25 @@ public class UXButton : UIBehaviour, IButton,
return;
}
// 聚焦时保持 Selected不回退到 Normal
bool isEventSystemSelected = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
bool isFocused = m_IsNavFocused || isEventSystemSelected;
if (m_Mode == ButtonModeType.Toggle && isFocused)
{
SetState(SelectionState.Selected);
return;
}
if (navigation.mode != UXNavigation.Mode.None)
{
if ((m_Mode == ButtonModeType.Normal && m_IsFocusLocked) ||
(m_Mode == ButtonModeType.Toggle && _mTogSelected))
{
SetState(SelectionState.Selected);
return;
}
}
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
}
@ -200,27 +301,240 @@ public class UXButton : UIBehaviour, IButton,
return;
PlayAudio(clickAudioClip);
HandleClick();
if (m_Mode == ButtonModeType.Normal)
{
if (navigation.mode != UXNavigation.Mode.None)
{
if (EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(gameObject, eventData);
SetLockedButton(this);
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick?.Invoke();
}
else
{
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick?.Invoke();
}
}
else
{
HandleClick();
if (navigation.mode != UXNavigation.Mode.None)
{
if (EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(gameObject, eventData);
SetLockedButton(this);
}
}
}
#endregion
#region Submit handling (Keyboard Submit)
public void OnSubmit(BaseEventData eventData)
{
SetState(SelectionState.Pressed);
if (_resetRoutine != null)
{
StopCoroutine(_resetRoutine);
_resetRoutine = null;
}
if (Animator)
{
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
}
if (EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(gameObject, eventData);
m_IsDown = true;
m_HasExitedWhileDown = false;
ForceSetState(SelectionState.Pressed);
PlayAudio(clickAudioClip);
if (m_Mode == ButtonModeType.Toggle)
{
_resetRoutine = StartCoroutine(SubmitToggleDeferredRoutine());
return;
}
HandleClick();
if (_resetRoutine != null)
StopCoroutine(_resetRoutine);
if (navigation.mode != UXNavigation.Mode.None)
SetLockedButton(this);
if (navigation.mode != UXNavigation.Mode.None && m_Mode == ButtonModeType.Normal)
_resetRoutine = StartCoroutine(SubmitAndLockRoutine());
else
_resetRoutine = StartCoroutine(ResetAfterSubmit());
}
#endregion
#region Selection / Navigation handling
public override void OnSelect(BaseEventData eventData)
{
base.OnSelect(eventData);
m_IsNavFocused = true;
if (s_LockedButton != null && s_LockedButton != this)
{
SetLockedButton(this);
return;
}
// 聚焦时无论逻辑是否选中Toggle 显示 Selected
if (m_Mode == ButtonModeType.Toggle)
{
SetState(SelectionState.Selected);
return;
}
if (m_IsDown)
{
SetState(SelectionState.Pressed);
}
else
{
if (m_IsFocusLocked)
SetState(SelectionState.Selected);
else
SetState(SelectionState.Highlighted);
}
}
public override void OnDeselect(BaseEventData eventData)
{
base.OnDeselect(eventData);
m_IsNavFocused = false;
bool selectionIsNull = EventSystem.current == null || EventSystem.current.currentSelectedGameObject == null;
if (selectionIsNull)
{
if (s_LockedButton == this)
s_LockedButton = null;
m_IsFocusLocked = false;
}
// Toggle 模式:如果逻辑上已选中则保持 Selected否则恢复 Normal
if (m_Mode == ButtonModeType.Toggle)
{
if (_mTogSelected)
SetState(SelectionState.Selected);
else
SetState(SelectionState.Normal);
return;
}
if (m_Mode == ButtonModeType.Normal && _mTogSelected)
{
SetState(SelectionState.Selected);
return;
}
SetState(SelectionState.Normal);
}
#endregion
#region Coroutines for submit/reset
private IEnumerator SubmitAndLockRoutine()
{
yield return _waitFadeDuration;
m_IsDown = false;
m_HasExitedWhileDown = false;
if (Animator)
{
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
}
SetLockedButton(this);
ApplyVisualState(SelectionState.Selected, false);
_resetRoutine = null;
}
private IEnumerator SubmitToggleDeferredRoutine()
{
yield return null;
yield return _waitFadeDuration;
HandleClick();
if (Animator)
{
foreach (int id in _animTriggerCache.Values)
Animator.ResetTrigger(id);
}
m_IsDown = false;
m_HasExitedWhileDown = false;
bool stillFocused = EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject;
if (stillFocused || m_IsNavFocused)
SetState(SelectionState.Selected);
else
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
if (navigation.mode != UXNavigation.Mode.None)
SetLockedButton(this);
_resetRoutine = null;
}
private IEnumerator ResetAfterSubmit()
{
yield return _waitFadeDuration;
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
_resetRoutine = null;
}
#endregion
#region Utility
private void ForceSetState(SelectionState state)
{
m_SelectionState = state;
ApplyVisualState(state, false);
}
#endregion
#region Logic
private bool CanProcess()
{
return m_Interactable &&
!(m_Mode == ButtonModeType.Toggle && Selected);
return m_Interactable;
}
private bool CanProcessEnter()
{
return m_Interactable;
}
private void HandleClick()
@ -236,7 +550,7 @@ public class UXButton : UIBehaviour, IButton,
}
else
{
InternalTogSelected = !Selected;
InternalTogSelected = !_mTogSelected;
}
}
@ -285,6 +599,7 @@ public class UXButton : UIBehaviour, IButton,
private void TweenColor(TransitionData data, Color color, bool instant)
{
if (data.targetGraphic == null) return;
if (Application.isPlaying)
{
data.targetGraphic.CrossFadeColor(
@ -318,19 +633,30 @@ public class UXButton : UIBehaviour, IButton,
Animator.SetTrigger(hash);
}
private void PlayAudio(AudioClip clip)
{
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
}
private IEnumerator ResetAfterSubmit()
#endregion
public void Focus()
{
yield return _waitFadeDuration;
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
_resetRoutine = null;
if (!IsInteractable())
return;
if (EventSystem.current != null)
{
EventSystem.current.SetSelectedGameObject(gameObject, new BaseEventData(EventSystem.current));
}
#endregion
m_IsNavFocused = true;
if ((s_LockedButton != null && s_LockedButton != this) || s_LockedButton == null)
{
SetLockedButton(this);
}
}
}

View File

@ -24,7 +24,7 @@ public class UXGroup : UIBehaviour
}
}
protected override void Awake() => ValidateGroup();
protected override void Start() => ValidateGroup();
protected override void OnDestroy()
{
@ -88,6 +88,7 @@ public class UXGroup : UIBehaviour
if (select) _current = btn;
}
if (_current) _current.Focus();
if (previous != _current)
onSelectedChanged?.Invoke(_current);
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 108cd2782bc4471ba364fb155525cec7
timeCreated: 1764742229

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX.UI.Extension.Utility
namespace AlicizaX.UI.Extension
{
internal static class SetPropertyUtility
{

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d3d40fd183ef410e995334bdca8e4638
timeCreated: 1764668263

View File

@ -0,0 +1,86 @@
using System;
using UnityEngine;
using UnityEngine.UI;
namespace AlicizaX.UI.Extension
{
[Serializable]
public struct UXNavigation : IEquatable<UXNavigation>
{
[Flags]
public enum Mode
{
None = 0,
Horizontal = 1,
Vertical = 2,
Automatic = 3,
Explicit = 4,
}
[SerializeField] private Mode m_Mode;
[Tooltip("Enables navigation to wrap around from last to first or first to last element. Does not work for automatic grid navigation")] [HideInInspector] [SerializeField]
private bool m_WrapAround;
[HideInInspector] [SerializeField] private UXSelectable m_SelectOnUp;
[HideInInspector] [SerializeField] private UXSelectable m_SelectOnDown;
[HideInInspector] [SerializeField] private UXSelectable m_SelectOnLeft;
[HideInInspector] [SerializeField] private UXSelectable m_SelectOnRight;
public Mode mode
{
get { return m_Mode; }
set { m_Mode = value; }
}
public bool wrapAround
{
get { return m_WrapAround; }
set { m_WrapAround = value; }
}
public UXSelectable selectOnUp
{
get { return m_SelectOnUp; }
set { m_SelectOnUp = value; }
}
public UXSelectable selectOnDown
{
get { return m_SelectOnDown; }
set { m_SelectOnDown = value; }
}
public UXSelectable selectOnLeft
{
get { return m_SelectOnLeft; }
set { m_SelectOnLeft = value; }
}
public UXSelectable selectOnRight
{
get { return m_SelectOnRight; }
set { m_SelectOnRight = value; }
}
static public UXNavigation defaultNavigation
{
get
{
var defaultNav = new UXNavigation();
defaultNav.m_Mode = Mode.Automatic;
defaultNav.m_WrapAround = false;
return defaultNav;
}
}
public bool Equals(UXNavigation other)
{
return mode == other.mode &&
selectOnUp == other.selectOnUp &&
selectOnDown == other.selectOnDown &&
selectOnLeft == other.selectOnLeft &&
selectOnRight == other.selectOnRight;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 46a321e196774407b8f17943e7b55c63
timeCreated: 1764667489

View File

@ -0,0 +1,267 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
namespace AlicizaX.UI.Extension
{
public class UXSelectable :
UIBehaviour,
IMoveHandler,
IPointerDownHandler, IPointerUpHandler,
IPointerEnterHandler, IPointerExitHandler,
ISelectHandler, IDeselectHandler
{
protected static UXSelectable[] s_Selectables = new UXSelectable[10];
protected static int s_SelectableCount = 0;
protected int m_CurrentIndex = -1;
protected bool m_EnableCalled = false;
protected bool isPointerInside { get; set; }
protected bool isPointerDown { get; set; }
protected bool hasSelection { get; set; }
protected bool m_GroupsAllowInteraction = true;
private readonly List<CanvasGroup> m_CanvasGroupCache = new List<CanvasGroup>();
[SerializeField] private UXNavigation m_Navigation = UXNavigation.defaultNavigation;
public UXNavigation navigation
{
get { return m_Navigation; }
set { if (SetPropertyUtility.SetStruct(ref m_Navigation, value)) OnSetProperty(); }
}
public static UXSelectable[] allSelectablesArray
{
get
{
UXSelectable[] temp = new UXSelectable[s_SelectableCount];
Array.Copy(s_Selectables, temp, s_SelectableCount);
return temp;
}
}
protected virtual void OnSetProperty()
{
// override if needed
}
protected override void OnEnable()
{
if (m_EnableCalled)
return;
base.OnEnable();
if (s_SelectableCount == s_Selectables.Length)
{
UXSelectable[] temp = new UXSelectable[s_Selectables.Length * 2];
Array.Copy(s_Selectables, temp, s_Selectables.Length);
s_Selectables = temp;
}
if (EventSystem.current && EventSystem.current.currentSelectedGameObject == gameObject)
{
hasSelection = true;
}
m_CurrentIndex = s_SelectableCount;
s_Selectables[m_CurrentIndex] = this;
s_SelectableCount++;
isPointerDown = false;
m_GroupsAllowInteraction = ParentGroupAllowsInteraction();
m_EnableCalled = true;
}
protected override void OnDisable()
{
if (!m_EnableCalled)
return;
s_SelectableCount--;
s_Selectables[s_SelectableCount].m_CurrentIndex = m_CurrentIndex;
s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount];
s_Selectables[s_SelectableCount] = null;
base.OnDisable();
m_EnableCalled = false;
}
protected void OnCanvasGroupChanged()
{
var parentGroupAllows = ParentGroupAllowsInteraction();
if (parentGroupAllows != m_GroupsAllowInteraction)
{
m_GroupsAllowInteraction = parentGroupAllows;
OnSetProperty();
}
}
protected bool ParentGroupAllowsInteraction()
{
Transform t = transform;
while (t != null)
{
t.GetComponents(m_CanvasGroupCache);
for (var i = 0; i < m_CanvasGroupCache.Count; i++)
{
if (m_CanvasGroupCache[i].enabled && !m_CanvasGroupCache[i].interactable)
return false;
if (m_CanvasGroupCache[i].ignoreParentGroups)
return true;
}
t = t.parent;
}
return true;
}
public virtual bool IsInteractable()
{
return m_GroupsAllowInteraction;
}
public UXSelectable FindSelectable(Vector3 dir)
{
dir = dir.normalized;
Vector3 localDir = Quaternion.Inverse(transform.rotation) * dir;
Vector3 pos = transform.TransformPoint(GetPointOnRectEdge(transform as RectTransform, localDir));
float maxScore = Mathf.NegativeInfinity;
float maxFurthestScore = Mathf.NegativeInfinity;
float score = 0;
bool wantsWrapAround = navigation.wrapAround && (m_Navigation.mode == UXNavigation.Mode.Vertical || m_Navigation.mode == UXNavigation.Mode.Horizontal);
UXSelectable bestPick = null;
UXSelectable bestFurthestPick = null;
for (int i = 0; i < s_SelectableCount; ++i)
{
UXSelectable sel = s_Selectables[i];
if (sel == this)
continue;
if (!sel.IsInteractable() || sel.navigation.mode == UXNavigation.Mode.None)
continue;
#if UNITY_EDITOR
if (Camera.current != null && !UnityEditor.SceneManagement.StageUtility.IsGameObjectRenderedByCamera(sel.gameObject, Camera.current))
continue;
#endif
var selRect = sel.transform as RectTransform;
Vector3 selCenter = selRect != null ? (Vector3)selRect.rect.center : Vector3.zero;
Vector3 myVector = sel.transform.TransformPoint(selCenter) - pos;
float dot = Vector3.Dot(dir, myVector);
if (wantsWrapAround && dot < 0)
{
score = -dot * myVector.sqrMagnitude;
if (score > maxFurthestScore)
{
maxFurthestScore = score;
bestFurthestPick = sel;
}
continue;
}
if (dot <= 0)
continue;
score = dot / myVector.sqrMagnitude;
if (score > maxScore)
{
maxScore = score;
bestPick = sel;
}
}
if (wantsWrapAround && null == bestPick) return bestFurthestPick;
return bestPick;
}
private static Vector3 GetPointOnRectEdge(RectTransform rect, Vector2 dir)
{
if (rect == null) return Vector3.zero;
if (dir != Vector2.zero) dir /= Mathf.Max(Mathf.Abs(dir.x), Mathf.Abs(dir.y));
dir = rect.rect.center + Vector2.Scale(rect.rect.size, dir * 0.5f);
return dir;
}
void Navigate(AxisEventData eventData, UXSelectable sel)
{
if (sel != null && sel.IsActive())
eventData.selectedObject = sel.gameObject;
}
public virtual UXSelectable FindSelectableOnLeft()
{
if (m_Navigation.mode == UXNavigation.Mode.Explicit)
return m_Navigation.selectOnLeft;
if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0)
return FindSelectable(transform.rotation * Vector3.left);
return null;
}
public virtual UXSelectable FindSelectableOnRight()
{
if (m_Navigation.mode == UXNavigation.Mode.Explicit)
return m_Navigation.selectOnRight;
if ((m_Navigation.mode & UXNavigation.Mode.Horizontal) != 0)
return FindSelectable(transform.rotation * Vector3.right);
return null;
}
public virtual UXSelectable FindSelectableOnUp()
{
if (m_Navigation.mode == UXNavigation.Mode.Explicit)
return m_Navigation.selectOnUp;
if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0)
return FindSelectable(transform.rotation * Vector3.up);
return null;
}
public virtual UXSelectable FindSelectableOnDown()
{
if (m_Navigation.mode == UXNavigation.Mode.Explicit)
return m_Navigation.selectOnDown;
if ((m_Navigation.mode & UXNavigation.Mode.Vertical) != 0)
return FindSelectable(transform.rotation * Vector3.down);
return null;
}
public virtual void OnMove(AxisEventData eventData)
{
switch (eventData.moveDir)
{
case MoveDirection.Right:
Navigate(eventData, FindSelectableOnRight());
break;
case MoveDirection.Up:
Navigate(eventData, FindSelectableOnUp());
break;
case MoveDirection.Left:
Navigate(eventData, FindSelectableOnLeft());
break;
case MoveDirection.Down:
Navigate(eventData, FindSelectableOnDown());
break;
}
}
// default handlers to be overridden
public virtual void OnPointerDown(PointerEventData eventData) { }
public virtual void OnPointerUp(PointerEventData eventData) { }
public virtual void OnPointerEnter(PointerEventData eventData) { }
public virtual void OnPointerExit(PointerEventData eventData) { }
public virtual void OnSelect(BaseEventData eventData) { hasSelection = true; }
public virtual void OnDeselect(BaseEventData eventData) { hasSelection = false; }
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0e3d476361ae4f1ea2e5143663661f3c
timeCreated: 1764667719

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 2997e0ea45404b2bad0969025eb613a5
timeCreated: 1760407867

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: baab95df60ce49af96e3b0efcf82ed30
timeCreated: 1760407873