com.alicizax.unity.ui.exten.../Runtime/UXComponent/Selectable/UXSelectable.cs
陈思海 32c0fc13fd 重构
1.重构UXButton
2.主Transition有UXSeletable负责 ,同时兼具导航
3.新增UXSlider  支持平滑过渡 适配手柄
4.优化部分逻辑bug
2025-12-09 15:08:41 +08:00

653 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
[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,
IMoveHandler,
IPointerDownHandler, IPointerUpHandler,
IPointerEnterHandler, IPointerExitHandler,
ISelectHandler, IDeselectHandler
{
[Serializable]
public enum SelectionState
{
Normal,
Highlighted,
Pressed,
Selected,
Disabled,
}
#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)
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>();
// 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
public UXNavigation navigation
{
get { return m_Navigation; }
set
{
if (SetPropertyUtility.SetStruct(ref m_Navigation, value))
OnSetProperty();
}
}
public bool Interactable
{
get => m_Interactable;
set
{
if (m_Interactable == value) return;
m_Interactable = value;
OnSetProperty();
}
}
public static UXSelectable[] allSelectablesArray
{
get
{
UXSelectable[] temp = new UXSelectable[s_SelectableCount];
Array.Copy(s_Selectables, temp, s_SelectableCount);
return temp;
}
}
#endregion
#region Unity Lifecycle
protected override void Awake()
{
base.Awake();
// nothing specific here; subclasses may initialize visuals
}
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;
// Ensure visual state matches current settings immediately
OnSetProperty();
}
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;
InstantClearState();
base.OnDisable();
m_EnableCalled = false;
}
protected void OnCanvasGroupChanged()
{
var parentGroupAllows = ParentGroupAllowsInteraction();
if (parentGroupAllows != m_GroupsAllowInteraction)
{
m_GroupsAllowInteraction = parentGroupAllows;
OnSetProperty();
}
}
#endregion
#region Groups & Interactable
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 && m_Interactable;
}
#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
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;
}
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;
}
}
void Navigate(AxisEventData eventData, UXSelectable sel)
{
if (sel != null && sel.IsActive())
eventData.selectedObject = sel.gameObject;
}
#endregion
#region Pointer / Select base implementations (update flags + evaluate)
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);
isPointerDown = true;
EvaluateAndTransitionToSelectionState();
}
public virtual void OnPointerUp(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
isPointerDown = false;
EvaluateAndTransitionToSelectionState();
}
public virtual void OnPointerEnter(PointerEventData eventData)
{
isPointerInside = true;
EvaluateAndTransitionToSelectionState();
}
public virtual void OnPointerExit(PointerEventData eventData)
{
isPointerInside = false;
EvaluateAndTransitionToSelectionState();
}
public virtual void OnSelect(BaseEventData eventData)
{
hasSelection = true;
EvaluateAndTransitionToSelectionState();
}
public virtual void OnDeselect(BaseEventData eventData)
{
hasSelection = false;
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;
}
}
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
}
}