新增UXSelectable 。对UXButton组件的手柄支持扩展
This commit is contained in:
parent
dfef3f5199
commit
74aa459a38
@ -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)
|
||||
{
|
||||
|
||||
@ -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_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
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 UnityEngine;
|
||||
|
||||
namespace AlicizaX.UI.Extension.Utility
|
||||
namespace AlicizaX.UI.Extension
|
||||
{
|
||||
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