新增UXSelectable 。对UXButton组件的手柄支持扩展
This commit is contained in:
parent
dfef3f5199
commit
74aa459a38
@ -1,7 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using AlicizaX.UI.Extension;
|
||||||
using AlicizaX.UI.Extension.Editor;
|
using AlicizaX.UI.Extension.Editor;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
|
using UnityEditor.SceneManagement;
|
||||||
using UnityEditorInternal;
|
using UnityEditorInternal;
|
||||||
using UnityEditor.UI;
|
using UnityEditor.UI;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@ -19,6 +22,11 @@ internal class UXButtonEditor : Editor
|
|||||||
Event
|
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_Interactable;
|
||||||
private SerializedProperty m_Mode;
|
private SerializedProperty m_Mode;
|
||||||
private SerializedProperty m_OnValueChanged;
|
private SerializedProperty m_OnValueChanged;
|
||||||
@ -40,6 +48,8 @@ internal class UXButtonEditor : Editor
|
|||||||
|
|
||||||
private SerializedProperty hoverAudioClip;
|
private SerializedProperty hoverAudioClip;
|
||||||
private SerializedProperty clickAudioClip;
|
private SerializedProperty clickAudioClip;
|
||||||
|
|
||||||
|
private SerializedProperty m_Navigation;
|
||||||
private UXButton mTarget;
|
private UXButton mTarget;
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
@ -54,13 +64,103 @@ internal class UXButtonEditor : Editor
|
|||||||
m_TransitionData = serializedObject.FindProperty("m_TransitionData");
|
m_TransitionData = serializedObject.FindProperty("m_TransitionData");
|
||||||
m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions");
|
m_ChildTransitions = serializedObject.FindProperty("m_ChildTransitions");
|
||||||
m_SelectionState = serializedObject.FindProperty("m_SelectionState");
|
m_SelectionState = serializedObject.FindProperty("m_SelectionState");
|
||||||
|
m_Navigation = serializedObject.FindProperty("m_Navigation");
|
||||||
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
|
hoverAudioClip = serializedObject.FindProperty("hoverAudioClip");
|
||||||
clickAudioClip = serializedObject.FindProperty("clickAudioClip");
|
clickAudioClip = serializedObject.FindProperty("clickAudioClip");
|
||||||
|
|
||||||
CreateChildTransitionList();
|
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()
|
private void CreateChildTransitionList()
|
||||||
{
|
{
|
||||||
m_ChildTransitionList = new ReorderableList(serializedObject, m_ChildTransitions, true, false, true, true);
|
m_ChildTransitionList = new ReorderableList(serializedObject, m_ChildTransitions, true, false, true, true);
|
||||||
@ -198,8 +298,23 @@ internal class UXButtonEditor : Editor
|
|||||||
m_Mode.enumValueIndex = (int)modeType;
|
m_Mode.enumValueIndex = (int)modeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
EditorGUI.EndDisabledGroup();
|
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");
|
var interactable = GUILayoutHelper.DrawToggle(m_Interactable.boolValue, customSkin, "Interactable");
|
||||||
if (interactable != m_Interactable.boolValue)
|
if (interactable != m_Interactable.boolValue)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AlicizaX.UI.Extension;
|
using AlicizaX.UI.Extension;
|
||||||
using AlicizaX.UI.Extension.Utility;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Events;
|
using UnityEngine.Events;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
@ -37,9 +36,7 @@ internal enum SelectionState
|
|||||||
|
|
||||||
[ExecuteInEditMode]
|
[ExecuteInEditMode]
|
||||||
[DisallowMultipleComponent]
|
[DisallowMultipleComponent]
|
||||||
public class UXButton : UIBehaviour, IButton,
|
public class UXButton : UXSelectable, IButton, IPointerClickHandler, ISubmitHandler
|
||||||
IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler,
|
|
||||||
IPointerExitHandler, IPointerClickHandler, ISubmitHandler
|
|
||||||
{
|
{
|
||||||
#region Serialized Fields
|
#region Serialized Fields
|
||||||
|
|
||||||
@ -65,6 +62,13 @@ public class UXButton : UIBehaviour, IButton,
|
|||||||
private Coroutine _resetRoutine;
|
private Coroutine _resetRoutine;
|
||||||
private WaitForSeconds _waitFadeDuration;
|
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()
|
private static readonly Dictionary<string, int> _animTriggerCache = new()
|
||||||
{
|
{
|
||||||
{ "Normal", Animator.StringToHash("Normal") },
|
{ "Normal", Animator.StringToHash("Normal") },
|
||||||
@ -112,9 +116,25 @@ public class UXButton : UIBehaviour, IButton,
|
|||||||
if (_mTogSelected == value) return;
|
if (_mTogSelected == value) return;
|
||||||
_mTogSelected = value;
|
_mTogSelected = value;
|
||||||
m_OnValueChanged?.Invoke(value);
|
m_OnValueChanged?.Invoke(value);
|
||||||
|
|
||||||
|
// ------------- 关键修复 -------------
|
||||||
|
// 如果当前控件处于聚焦(由导航/SetSelected 进入),那么视觉上应保持 Selected(无论逻辑是否为 selected)。
|
||||||
|
// 聚焦判定使用 m_IsNavFocused(OnSelect/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);
|
SetState(value ? SelectionState.Selected : SelectionState.Normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Button.ButtonClickedEvent onClick
|
public Button.ButtonClickedEvent onClick
|
||||||
{
|
{
|
||||||
@ -146,43 +166,105 @@ public class UXButton : UIBehaviour, IButton,
|
|||||||
base.OnDestroy();
|
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
|
#endregion
|
||||||
|
|
||||||
#region Pointer Handlers
|
#region Pointer Handlers
|
||||||
|
|
||||||
public void OnPointerDown(PointerEventData eventData)
|
public override void OnPointerDown(PointerEventData eventData)
|
||||||
{
|
{
|
||||||
if (!CanProcess()) return;
|
if (!CanProcess()) return;
|
||||||
m_IsDown = true;
|
m_IsDown = true;
|
||||||
|
m_HasExitedWhileDown = false;
|
||||||
SetState(SelectionState.Pressed);
|
SetState(SelectionState.Pressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnPointerUp(PointerEventData eventData)
|
public override void OnPointerUp(PointerEventData eventData)
|
||||||
{
|
{
|
||||||
if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
|
if (!m_Interactable || eventData.button != PointerEventData.InputButton.Left)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
m_IsDown = false;
|
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);
|
SetState(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnPointerEnter(PointerEventData eventData)
|
public override void OnPointerEnter(PointerEventData eventData)
|
||||||
{
|
{
|
||||||
if (!CanProcess()) return;
|
if (!CanProcessEnter()) return;
|
||||||
m_HasExitedWhileDown = false;
|
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;
|
if (m_IsDown) return;
|
||||||
|
|
||||||
SetState(SelectionState.Highlighted);
|
SetState(SelectionState.Highlighted);
|
||||||
PlayAudio(hoverAudioClip);
|
PlayAudio(hoverAudioClip);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnPointerExit(PointerEventData eventData)
|
public override void OnPointerExit(PointerEventData eventData)
|
||||||
{
|
{
|
||||||
if (!m_Interactable) return;
|
if (!m_Interactable) return;
|
||||||
if (m_IsDown)
|
if (m_IsDown)
|
||||||
@ -191,6 +273,25 @@ public class UXButton : UIBehaviour, IButton,
|
|||||||
return;
|
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);
|
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,27 +301,240 @@ public class UXButton : UIBehaviour, IButton,
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
PlayAudio(clickAudioClip);
|
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)
|
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();
|
HandleClick();
|
||||||
|
|
||||||
if (_resetRoutine != null)
|
if (navigation.mode != UXNavigation.Mode.None)
|
||||||
StopCoroutine(_resetRoutine);
|
SetLockedButton(this);
|
||||||
|
|
||||||
|
if (navigation.mode != UXNavigation.Mode.None && m_Mode == ButtonModeType.Normal)
|
||||||
|
_resetRoutine = StartCoroutine(SubmitAndLockRoutine());
|
||||||
|
else
|
||||||
_resetRoutine = StartCoroutine(ResetAfterSubmit());
|
_resetRoutine = StartCoroutine(ResetAfterSubmit());
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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
|
#region Logic
|
||||||
|
|
||||||
private bool CanProcess()
|
private bool CanProcess()
|
||||||
{
|
{
|
||||||
return m_Interactable &&
|
return m_Interactable;
|
||||||
!(m_Mode == ButtonModeType.Toggle && Selected);
|
}
|
||||||
|
|
||||||
|
private bool CanProcessEnter()
|
||||||
|
{
|
||||||
|
return m_Interactable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleClick()
|
private void HandleClick()
|
||||||
@ -236,7 +550,7 @@ public class UXButton : UIBehaviour, IButton,
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
InternalTogSelected = !Selected;
|
InternalTogSelected = !_mTogSelected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,6 +599,7 @@ public class UXButton : UIBehaviour, IButton,
|
|||||||
|
|
||||||
private void TweenColor(TransitionData data, Color color, bool instant)
|
private void TweenColor(TransitionData data, Color color, bool instant)
|
||||||
{
|
{
|
||||||
|
if (data.targetGraphic == null) return;
|
||||||
if (Application.isPlaying)
|
if (Application.isPlaying)
|
||||||
{
|
{
|
||||||
data.targetGraphic.CrossFadeColor(
|
data.targetGraphic.CrossFadeColor(
|
||||||
@ -318,19 +633,30 @@ public class UXButton : UIBehaviour, IButton,
|
|||||||
Animator.SetTrigger(hash);
|
Animator.SetTrigger(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void PlayAudio(AudioClip clip)
|
private void PlayAudio(AudioClip clip)
|
||||||
{
|
{
|
||||||
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
|
if (clip && UXComponentExtensionsHelper.AudioHelper != null)
|
||||||
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
|
UXComponentExtensionsHelper.AudioHelper.PlayAudio(clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator ResetAfterSubmit()
|
#endregion
|
||||||
|
|
||||||
|
|
||||||
|
public void Focus()
|
||||||
{
|
{
|
||||||
yield return _waitFadeDuration;
|
if (!IsInteractable())
|
||||||
SetState(_mTogSelected ? SelectionState.Selected : SelectionState.Normal);
|
return;
|
||||||
_resetRoutine = null;
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ public class UXGroup : UIBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Awake() => ValidateGroup();
|
protected override void Start() => ValidateGroup();
|
||||||
|
|
||||||
protected override void OnDestroy()
|
protected override void OnDestroy()
|
||||||
{
|
{
|
||||||
@ -88,6 +88,7 @@ public class UXGroup : UIBehaviour
|
|||||||
if (select) _current = btn;
|
if (select) _current = btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_current) _current.Focus();
|
||||||
if (previous != _current)
|
if (previous != _current)
|
||||||
onSelectedChanged?.Invoke(_current);
|
onSelectedChanged?.Invoke(_current);
|
||||||
}
|
}
|
||||||
|
|||||||
3
Runtime/UXComponent/Selectable.meta
Normal file
3
Runtime/UXComponent/Selectable.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 108cd2782bc4471ba364fb155525cec7
|
||||||
|
timeCreated: 1764742229
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace AlicizaX.UI.Extension.Utility
|
namespace AlicizaX.UI.Extension
|
||||||
{
|
{
|
||||||
internal static class SetPropertyUtility
|
internal static class SetPropertyUtility
|
||||||
{
|
{
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d3d40fd183ef410e995334bdca8e4638
|
||||||
|
timeCreated: 1764668263
|
||||||
86
Runtime/UXComponent/Selectable/UXNavigation.cs
Normal file
86
Runtime/UXComponent/Selectable/UXNavigation.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Runtime/UXComponent/Selectable/UXNavigation.cs.meta
Normal file
3
Runtime/UXComponent/Selectable/UXNavigation.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 46a321e196774407b8f17943e7b55c63
|
||||||
|
timeCreated: 1764667489
|
||||||
267
Runtime/UXComponent/Selectable/UXSelectable.cs
Normal file
267
Runtime/UXComponent/Selectable/UXSelectable.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Runtime/UXComponent/Selectable/UXSelectable.cs.meta
Normal file
3
Runtime/UXComponent/Selectable/UXSelectable.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0e3d476361ae4f1ea2e5143663661f3c
|
||||||
|
timeCreated: 1764667719
|
||||||
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 2997e0ea45404b2bad0969025eb613a5
|
|
||||||
timeCreated: 1760407867
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: baab95df60ce49af96e3b0efcf82ed30
|
|
||||||
timeCreated: 1760407873
|
|
||||||
Loading…
Reference in New Issue
Block a user