2025-12-03 17:26:55 +08:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using UnityEngine.EventSystems;
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
namespace UnityEngine.UI
|
2025-12-03 17:26:55 +08:00
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
[Serializable]
|
|
|
|
|
public class TransitionData
|
|
|
|
|
{
|
|
|
|
|
public Graphic targetGraphic;
|
|
|
|
|
public Selectable.Transition transition = Selectable.Transition.ColorTint;
|
|
|
|
|
public ColorBlock colors = ColorBlock.defaultColorBlock;
|
|
|
|
|
public SpriteState spriteState;
|
|
|
|
|
public AnimationTriggers animationTriggers = new AnimationTriggers();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[ExecuteAlways]
|
|
|
|
|
[SelectionBase]
|
|
|
|
|
[DisallowMultipleComponent]
|
|
|
|
|
public class UXSelectable : UIBehaviour,
|
2025-12-03 17:26:55 +08:00
|
|
|
IMoveHandler,
|
|
|
|
|
IPointerDownHandler, IPointerUpHandler,
|
|
|
|
|
IPointerEnterHandler, IPointerExitHandler,
|
|
|
|
|
ISelectHandler, IDeselectHandler
|
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
[Serializable]
|
|
|
|
|
public enum SelectionState
|
|
|
|
|
{
|
|
|
|
|
Normal,
|
|
|
|
|
Highlighted,
|
|
|
|
|
Pressed,
|
|
|
|
|
Selected,
|
|
|
|
|
Disabled,
|
|
|
|
|
}
|
2025-12-03 17:26:55 +08:00
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
#region Fields
|
|
|
|
|
|
|
|
|
|
[SerializeField] protected UXNavigation m_Navigation = UXNavigation.defaultNavigation;
|
|
|
|
|
|
|
|
|
|
[SerializeField] protected bool m_Interactable = true;
|
|
|
|
|
[SerializeField] protected TransitionData m_MainTransition = new TransitionData();
|
|
|
|
|
|
|
|
|
|
// current visual / logical selection state (now in base)
|
|
|
|
|
[SerializeField] protected SelectionState m_SelectionState = SelectionState.Normal;
|
|
|
|
|
|
|
|
|
|
// runtime event flags (used by currentSelectionState)
|
2025-12-03 17:26:55 +08:00
|
|
|
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>();
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
// registry for navigation find functions
|
|
|
|
|
protected static UXSelectable[] s_Selectables = new UXSelectable[10];
|
|
|
|
|
protected static int s_SelectableCount = 0;
|
|
|
|
|
protected int m_CurrentIndex = -1;
|
|
|
|
|
protected bool m_EnableCalled = false;
|
|
|
|
|
|
|
|
|
|
public Graphic targetGraphic
|
|
|
|
|
{
|
|
|
|
|
get { return m_MainTransition.targetGraphic; }
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (SetPropertyUtility.SetClass(ref m_MainTransition.targetGraphic, value)) OnSetProperty();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Properties
|
2025-12-05 19:03:35 +08:00
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
public UXNavigation navigation
|
|
|
|
|
{
|
|
|
|
|
get { return m_Navigation; }
|
2025-12-05 19:03:35 +08:00
|
|
|
set
|
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
if (SetPropertyUtility.SetStruct(ref m_Navigation, value))
|
|
|
|
|
OnSetProperty();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool Interactable
|
|
|
|
|
{
|
|
|
|
|
get => m_Interactable;
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
if (m_Interactable == value) return;
|
|
|
|
|
m_Interactable = value;
|
|
|
|
|
OnSetProperty();
|
2025-12-05 19:03:35 +08:00
|
|
|
}
|
2025-12-03 17:26:55 +08:00
|
|
|
}
|
2025-12-05 19:03:35 +08:00
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
public static UXSelectable[] allSelectablesArray
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
UXSelectable[] temp = new UXSelectable[s_SelectableCount];
|
|
|
|
|
Array.Copy(s_Selectables, temp, s_SelectableCount);
|
|
|
|
|
return temp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
#endregion
|
2025-12-03 17:26:55 +08:00
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
#region Unity Lifecycle
|
|
|
|
|
|
|
|
|
|
protected override void Awake()
|
2025-12-03 17:26:55 +08:00
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
base.Awake();
|
|
|
|
|
// nothing specific here; subclasses may initialize visuals
|
2025-12-03 17:26:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2025-12-09 15:08:41 +08:00
|
|
|
|
|
|
|
|
// Ensure visual state matches current settings immediately
|
|
|
|
|
OnSetProperty();
|
2025-12-03 17:26:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
InstantClearState();
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
base.OnDisable();
|
|
|
|
|
m_EnableCalled = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected void OnCanvasGroupChanged()
|
|
|
|
|
{
|
|
|
|
|
var parentGroupAllows = ParentGroupAllowsInteraction();
|
|
|
|
|
if (parentGroupAllows != m_GroupsAllowInteraction)
|
|
|
|
|
{
|
|
|
|
|
m_GroupsAllowInteraction = parentGroupAllows;
|
|
|
|
|
OnSetProperty();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Groups & Interactable
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
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()
|
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
return m_GroupsAllowInteraction && m_Interactable;
|
2025-12-03 17:26:55 +08:00
|
|
|
}
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Property change handling & SelectionState API
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Called when a property that affects visuals changes (navigation/interactable/animation/etc).
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected virtual void OnSetProperty()
|
|
|
|
|
{
|
|
|
|
|
// If not interactable => Disabled state
|
|
|
|
|
if (!IsInteractable())
|
|
|
|
|
{
|
|
|
|
|
m_SelectionState = SelectionState.Disabled;
|
|
|
|
|
DoStateTransition(SelectionState.Disabled, false);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// If previously disabled, restore to Normal
|
|
|
|
|
if (m_SelectionState == SelectionState.Disabled)
|
|
|
|
|
m_SelectionState = SelectionState.Normal;
|
|
|
|
|
|
|
|
|
|
// Apply current selection state
|
|
|
|
|
DoStateTransition(m_SelectionState, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Computed selection state based on pointer / selection flags and interactability.
|
|
|
|
|
/// Mirrors Unity's Selectable.currentSelectionState behavior.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected SelectionState currentSelectionState
|
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
if (!IsInteractable())
|
|
|
|
|
return SelectionState.Disabled;
|
|
|
|
|
if (isPointerDown)
|
|
|
|
|
return SelectionState.Pressed;
|
|
|
|
|
if (hasSelection)
|
|
|
|
|
return SelectionState.Selected;
|
|
|
|
|
if (isPointerInside)
|
|
|
|
|
return SelectionState.Highlighted;
|
|
|
|
|
return SelectionState.Normal;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Evaluate and transition to the computed selection state. Call after changing pointer/selection flags.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected void EvaluateAndTransitionToSelectionState()
|
|
|
|
|
{
|
|
|
|
|
if (!IsActive() || !IsInteractable())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
DoStateTransition(currentSelectionState, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Force apply visual state immediately.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected void ForceSetState(SelectionState state)
|
|
|
|
|
{
|
|
|
|
|
m_SelectionState = state;
|
|
|
|
|
DoStateTransition(state, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Clear internal state and restore visuals (used on disable / focus lost).
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected virtual void InstantClearState()
|
|
|
|
|
{
|
|
|
|
|
// reset flags
|
|
|
|
|
isPointerInside = false;
|
|
|
|
|
isPointerDown = false;
|
|
|
|
|
hasSelection = false;
|
|
|
|
|
|
|
|
|
|
// restore visuals based on main transition's normal state
|
|
|
|
|
if (m_MainTransition == null) return;
|
|
|
|
|
|
|
|
|
|
switch (m_MainTransition.transition)
|
|
|
|
|
{
|
|
|
|
|
case Selectable.Transition.ColorTint:
|
|
|
|
|
TweenColor(m_MainTransition, m_MainTransition.colors.normalColor * m_MainTransition.colors.colorMultiplier, true);
|
|
|
|
|
break;
|
|
|
|
|
case Selectable.Transition.SpriteSwap:
|
|
|
|
|
SwapSprite(m_MainTransition, null);
|
|
|
|
|
break;
|
|
|
|
|
case Selectable.Transition.Animation:
|
|
|
|
|
PlayAnimation(m_MainTransition.animationTriggers.normalTrigger);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_SelectionState = SelectionState.Normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Core mapping from SelectionState -> TransitionData and execution.
|
|
|
|
|
/// Subclasses can override PlayAnimation or ApplyVisualState for custom behavior.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="state"></param>
|
|
|
|
|
/// <param name="instant"></param>
|
|
|
|
|
protected virtual void DoStateTransition(SelectionState state, bool instant)
|
|
|
|
|
{
|
|
|
|
|
if (!gameObject.activeInHierarchy)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (m_MainTransition == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
Color tintColor = Color.white;
|
|
|
|
|
Sprite sprite = null;
|
|
|
|
|
string trigger = null;
|
|
|
|
|
|
|
|
|
|
switch (state)
|
|
|
|
|
{
|
|
|
|
|
case SelectionState.Normal:
|
|
|
|
|
tintColor = m_MainTransition.colors.normalColor;
|
|
|
|
|
sprite = m_MainTransition.spriteState.highlightedSprite;
|
|
|
|
|
trigger = m_MainTransition.animationTriggers.normalTrigger;
|
|
|
|
|
break;
|
|
|
|
|
case SelectionState.Highlighted:
|
|
|
|
|
tintColor = m_MainTransition.colors.highlightedColor;
|
|
|
|
|
sprite = m_MainTransition.spriteState.highlightedSprite;
|
|
|
|
|
trigger = m_MainTransition.animationTriggers.highlightedTrigger;
|
|
|
|
|
break;
|
|
|
|
|
case SelectionState.Pressed:
|
|
|
|
|
tintColor = m_MainTransition.colors.pressedColor;
|
|
|
|
|
sprite = m_MainTransition.spriteState.pressedSprite;
|
|
|
|
|
trigger = m_MainTransition.animationTriggers.pressedTrigger;
|
|
|
|
|
break;
|
|
|
|
|
case SelectionState.Selected:
|
|
|
|
|
tintColor = m_MainTransition.colors.selectedColor;
|
|
|
|
|
sprite = m_MainTransition.spriteState.selectedSprite;
|
|
|
|
|
trigger = m_MainTransition.animationTriggers.selectedTrigger;
|
|
|
|
|
break;
|
|
|
|
|
case SelectionState.Disabled:
|
|
|
|
|
tintColor = m_MainTransition.colors.disabledColor;
|
|
|
|
|
sprite = m_MainTransition.spriteState.disabledSprite;
|
|
|
|
|
trigger = m_MainTransition.animationTriggers.disabledTrigger;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Execute appropriate transition type
|
|
|
|
|
switch (m_MainTransition.transition)
|
|
|
|
|
{
|
|
|
|
|
case Selectable.Transition.ColorTint:
|
|
|
|
|
TweenColor(m_MainTransition, tintColor * m_MainTransition.colors.colorMultiplier, instant);
|
|
|
|
|
break;
|
|
|
|
|
case Selectable.Transition.SpriteSwap:
|
|
|
|
|
SwapSprite(m_MainTransition, sprite);
|
|
|
|
|
break;
|
|
|
|
|
case Selectable.Transition.Animation:
|
|
|
|
|
PlayAnimation(trigger);
|
|
|
|
|
break;
|
|
|
|
|
case Selectable.Transition.None:
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// keep serialized state in sync
|
|
|
|
|
m_SelectionState = state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Navigation helpers
|
|
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
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;
|
|
|
|
|
}
|
2025-12-05 19:03:35 +08:00
|
|
|
|
2025-12-03 17:26:55 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 15:08:41 +08:00
|
|
|
void Navigate(AxisEventData eventData, UXSelectable sel)
|
|
|
|
|
{
|
|
|
|
|
if (sel != null && sel.IsActive())
|
|
|
|
|
eventData.selectedObject = sel.gameObject;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Pointer / Select base implementations (update flags + evaluate)
|
|
|
|
|
|
2025-12-05 19:03:35 +08:00
|
|
|
public virtual void OnPointerDown(PointerEventData eventData)
|
|
|
|
|
{
|
|
|
|
|
if (eventData.button != PointerEventData.InputButton.Left)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (IsInteractable() && navigation.mode != UXNavigation.Mode.None && EventSystem.current != null)
|
|
|
|
|
EventSystem.current.SetSelectedGameObject(gameObject, eventData);
|
2025-12-09 15:08:41 +08:00
|
|
|
|
|
|
|
|
isPointerDown = true;
|
|
|
|
|
EvaluateAndTransitionToSelectionState();
|
2025-12-05 19:03:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public virtual void OnPointerUp(PointerEventData eventData)
|
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
if (eventData.button != PointerEventData.InputButton.Left)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
isPointerDown = false;
|
|
|
|
|
EvaluateAndTransitionToSelectionState();
|
2025-12-05 19:03:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public virtual void OnPointerEnter(PointerEventData eventData)
|
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
isPointerInside = true;
|
|
|
|
|
EvaluateAndTransitionToSelectionState();
|
2025-12-05 19:03:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public virtual void OnPointerExit(PointerEventData eventData)
|
|
|
|
|
{
|
2025-12-09 15:08:41 +08:00
|
|
|
isPointerInside = false;
|
|
|
|
|
EvaluateAndTransitionToSelectionState();
|
2025-12-05 19:03:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public virtual void OnSelect(BaseEventData eventData)
|
|
|
|
|
{
|
|
|
|
|
hasSelection = true;
|
2025-12-09 15:08:41 +08:00
|
|
|
EvaluateAndTransitionToSelectionState();
|
2025-12-05 19:03:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public virtual void OnDeselect(BaseEventData eventData)
|
|
|
|
|
{
|
|
|
|
|
hasSelection = false;
|
2025-12-09 15:08:41 +08:00
|
|
|
EvaluateAndTransitionToSelectionState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Visual transition helpers (main transition + low level ops)
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// High-level API: apply main transition visuals (child classes can override, UXButton overrides to add child transitions).
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="state"></param>
|
|
|
|
|
/// <param name="instant"></param>
|
|
|
|
|
public virtual void ApplyVisualState(SelectionState state, bool instant)
|
|
|
|
|
{
|
|
|
|
|
if (m_MainTransition != null)
|
|
|
|
|
ApplyTransition(m_MainTransition, state, instant);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Apply a single TransitionData (handles ColorTint / SpriteSwap / Animation).
|
|
|
|
|
/// Animation triggering calls PlayAnimation (virtual).
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected void ApplyTransition(TransitionData data, SelectionState state, bool instant)
|
|
|
|
|
{
|
|
|
|
|
if (data == null) return;
|
|
|
|
|
if (data.targetGraphic == null && data.transition != Selectable.Transition.Animation)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
Color color = Color.white;
|
|
|
|
|
Sprite sprite = null;
|
|
|
|
|
string trigger = null;
|
|
|
|
|
|
|
|
|
|
switch (state)
|
|
|
|
|
{
|
|
|
|
|
case SelectionState.Normal:
|
|
|
|
|
color = data.colors.normalColor;
|
|
|
|
|
sprite = data.spriteState.highlightedSprite;
|
|
|
|
|
trigger = data.animationTriggers.normalTrigger;
|
|
|
|
|
break;
|
|
|
|
|
case SelectionState.Highlighted:
|
|
|
|
|
color = data.colors.highlightedColor;
|
|
|
|
|
sprite = data.spriteState.highlightedSprite;
|
|
|
|
|
trigger = data.animationTriggers.highlightedTrigger;
|
|
|
|
|
break;
|
|
|
|
|
case SelectionState.Pressed:
|
|
|
|
|
color = data.colors.pressedColor;
|
|
|
|
|
sprite = data.spriteState.pressedSprite;
|
|
|
|
|
trigger = data.animationTriggers.pressedTrigger;
|
|
|
|
|
break;
|
|
|
|
|
case SelectionState.Selected:
|
|
|
|
|
color = data.colors.selectedColor;
|
|
|
|
|
sprite = data.spriteState.selectedSprite;
|
|
|
|
|
trigger = data.animationTriggers.selectedTrigger;
|
|
|
|
|
break;
|
|
|
|
|
case SelectionState.Disabled:
|
|
|
|
|
color = data.colors.disabledColor;
|
|
|
|
|
sprite = data.spriteState.disabledSprite;
|
|
|
|
|
trigger = data.animationTriggers.disabledTrigger;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (data.transition)
|
|
|
|
|
{
|
|
|
|
|
case Selectable.Transition.ColorTint:
|
|
|
|
|
TweenColor(data, color * data.colors.colorMultiplier, instant);
|
|
|
|
|
break;
|
|
|
|
|
case Selectable.Transition.SpriteSwap:
|
|
|
|
|
SwapSprite(data, sprite);
|
|
|
|
|
break;
|
|
|
|
|
case Selectable.Transition.Animation:
|
|
|
|
|
PlayAnimation(trigger);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-12-05 19:03:35 +08:00
|
|
|
}
|
2025-12-09 15:08:41 +08:00
|
|
|
|
|
|
|
|
protected void TweenColor(TransitionData data, Color color, bool instant)
|
|
|
|
|
{
|
|
|
|
|
if (data == null || data.targetGraphic == null) return;
|
|
|
|
|
data.targetGraphic.CrossFadeColor(
|
|
|
|
|
color,
|
|
|
|
|
instant ? 0f : data.colors.fadeDuration,
|
|
|
|
|
true,
|
|
|
|
|
true
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static void SwapSprite(TransitionData data, Sprite sprite)
|
|
|
|
|
{
|
|
|
|
|
if (data == null) return;
|
|
|
|
|
if (data.targetGraphic is Image img)
|
|
|
|
|
img.overrideSprite = sprite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Subclasses override this to trigger Animator triggers, etc.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="trigger"></param>
|
|
|
|
|
protected virtual void PlayAnimation(string trigger)
|
|
|
|
|
{
|
|
|
|
|
// base does nothing — subclasses (e.g. UXButton) can override to use Animator.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
2025-12-03 17:26:55 +08:00
|
|
|
}
|
|
|
|
|
}
|