using System;
using System.Collections.Generic;
using UnityEngine.Serialization;
using UnityEngine.EventSystems;
namespace UnityEngine.UI
{
[AddComponentMenu("UI/Selectable", 35)]
[ExecuteAlways]
[SelectionBase]
[DisallowMultipleComponent]
///
/// Simple selectable object - derived from to create a selectable control.
///
public class Selectable
:
UIBehaviour,
IMoveHandler,
IPointerDownHandler, IPointerUpHandler,
IPointerEnterHandler, IPointerExitHandler,
ISelectHandler, IDeselectHandler
{
protected static Selectable[] s_Selectables = new Selectable[10];
protected static int s_SelectableCount = 0;
private bool m_EnableCalled = false;
///
/// Copy of the array of all the selectable objects currently active in the scene.
///
///
///
///
///
///
public static Selectable[] allSelectablesArray
{
get
{
Selectable[] temp = new Selectable[s_SelectableCount];
Array.Copy(s_Selectables, temp, s_SelectableCount);
return temp;
}
}
///
/// How many selectable elements are currently active.
///
public static int allSelectableCount { get { return s_SelectableCount; } }
///
/// A List instance of the allSelectablesArray to maintain API compatibility.
///
[Obsolete("Replaced with allSelectablesArray to have better performance when disabling a element", false)]
public static List allSelectables
{
get
{
return new List(allSelectablesArray);
}
}
///
/// Non allocating version for getting the all selectables.
/// If selectables.Length is less then s_SelectableCount only selectables.Length elments will be copied which
/// could result in a incomplete list of elements.
///
/// The array to be filled with current selectable objects
/// The number of element copied.
///
///
///
///
///
public static int AllSelectablesNoAlloc(Selectable[] selectables)
{
int copyCount = selectables.Length < s_SelectableCount ? selectables.Length : s_SelectableCount;
Array.Copy(s_Selectables, selectables, copyCount);
return copyCount;
}
// Navigation information.
[FormerlySerializedAs("navigation")]
[SerializeField]
private Navigation m_Navigation = Navigation.defaultNavigation;
///
///Transition mode for a Selectable.
///
public enum Transition
{
///
/// No Transition.
///
None,
///
/// Use an color tint transition.
///
ColorTint,
///
/// Use a sprite swap transition.
///
SpriteSwap,
///
/// Use an animation transition.
///
Animation
}
// Type of the transition that occurs when the button state changes.
[FormerlySerializedAs("transition")]
[SerializeField]
private Transition m_Transition = Transition.ColorTint;
// Colors used for a color tint-based transition.
[FormerlySerializedAs("colors")]
[SerializeField]
private ColorBlock m_Colors = ColorBlock.defaultColorBlock;
// Sprites used for a Image swap-based transition.
[FormerlySerializedAs("spriteState")]
[SerializeField]
private SpriteState m_SpriteState;
[FormerlySerializedAs("animationTriggers")]
[SerializeField]
private AnimationTriggers m_AnimationTriggers = new AnimationTriggers();
[Tooltip("Can the Selectable be interacted with?")]
[SerializeField]
private bool m_Interactable = true;
// Graphic that will be colored.
[FormerlySerializedAs("highlightGraphic")]
[FormerlySerializedAs("m_HighlightGraphic")]
[SerializeField]
private Graphic m_TargetGraphic;
private bool m_GroupsAllowInteraction = true;
protected int m_CurrentIndex = -1;
///
/// The Navigation setting for this selectable object.
///
///
///
///
///
///
public Navigation navigation { get { return m_Navigation; } set { if (SetPropertyUtility.SetStruct(ref m_Navigation, value)) OnSetProperty(); } }
///
/// The type of transition that will be applied to the targetGraphic when the state changes.
///
///
///
///
///
///
public Transition transition { get { return m_Transition; } set { if (SetPropertyUtility.SetStruct(ref m_Transition, value)) OnSetProperty(); } }
///
/// The ColorBlock for this selectable object.
///
///
/// Modifications will not be visible if transition is not ColorTint.
///
///
///
///
///
///
public ColorBlock colors { get { return m_Colors; } set { if (SetPropertyUtility.SetStruct(ref m_Colors, value)) OnSetProperty(); } }
///
/// The SpriteState for this selectable object.
///
///
/// Modifications will not be visible if transition is not SpriteSwap.
///
///
///
///
///
///
public SpriteState spriteState { get { return m_SpriteState; } set { if (SetPropertyUtility.SetStruct(ref m_SpriteState, value)) OnSetProperty(); } }
///
/// The AnimationTriggers for this selectable object.
///
///
/// Modifications will not be visible if transition is not Animation.
///
public AnimationTriggers animationTriggers { get { return m_AnimationTriggers; } set { if (SetPropertyUtility.SetClass(ref m_AnimationTriggers, value)) OnSetProperty(); } }
///
/// Graphic that will be transitioned upon.
///
///
///
///
///
///
public Graphic targetGraphic { get { return m_TargetGraphic; } set { if (SetPropertyUtility.SetClass(ref m_TargetGraphic, value)) OnSetProperty(); } }
///
/// Is this object interactable.
///
///
///
///
///
///
public bool interactable
{
get { return m_Interactable; }
set
{
if (SetPropertyUtility.SetStruct(ref m_Interactable, value))
{
if (!m_Interactable && EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject)
EventSystem.current.SetSelectedGameObject(null);
OnSetProperty();
}
}
}
private bool isPointerInside { get; set; }
private bool isPointerDown { get; set; }
private bool hasSelection { get; set; }
protected Selectable()
{}
///
/// Convenience function that converts the referenced Graphic to a Image, if possible.
///
public Image image
{
get { return m_TargetGraphic as Image; }
set { m_TargetGraphic = value; }
}
///
/// Convenience function to get the Animator component on the GameObject.
///
///
///
///
///
///
#if PACKAGE_ANIMATION
public Animator animator
{
get { return GetComponent(); }
}
#endif
protected override void Awake()
{
if (m_TargetGraphic == null)
m_TargetGraphic = GetComponent();
}
private readonly List m_CanvasGroupCache = new List();
protected override void OnCanvasGroupChanged()
{
var parentGroupAllowsInteraction = ParentGroupAllowsInteraction();
if (parentGroupAllowsInteraction != m_GroupsAllowInteraction)
{
m_GroupsAllowInteraction = parentGroupAllowsInteraction;
OnSetProperty();
}
}
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;
}
///
/// Is the object interactable.
///
///
///
///
///
///
public virtual bool IsInteractable()
{
return m_GroupsAllowInteraction && m_Interactable;
}
// Call from unity if animation properties have changed
protected override void OnDidApplyAnimationProperties()
{
OnSetProperty();
}
// Select on enable and add to the list.
protected override void OnEnable()
{
//Check to avoid multiple OnEnable() calls for each selectable
if (m_EnableCalled)
return;
base.OnEnable();
if (s_SelectableCount == s_Selectables.Length)
{
Selectable[] temp = new Selectable[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();
DoStateTransition(currentSelectionState, true);
m_EnableCalled = true;
}
protected override void OnTransformParentChanged()
{
base.OnTransformParentChanged();
// If our parenting changes figure out if we are under a new CanvasGroup.
OnCanvasGroupChanged();
}
private void OnSetProperty()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
DoStateTransition(currentSelectionState, true);
else
#endif
DoStateTransition(currentSelectionState, false);
}
// Remove from the list.
protected override void OnDisable()
{
//Check to avoid multiple OnDisable() calls for each selectable
if (!m_EnableCalled)
return;
s_SelectableCount--;
// Update the last elements index to be this index
s_Selectables[s_SelectableCount].m_CurrentIndex = m_CurrentIndex;
// Swap the last element and this element
s_Selectables[m_CurrentIndex] = s_Selectables[s_SelectableCount];
// null out last element.
s_Selectables[s_SelectableCount] = null;
InstantClearState();
base.OnDisable();
m_EnableCalled = false;
}
void OnApplicationFocus(bool hasFocus)
{
if (!hasFocus && IsPressed())
{
InstantClearState();
}
}
#if UNITY_EDITOR
protected override void OnValidate()
{
base.OnValidate();
m_Colors.fadeDuration = Mathf.Max(m_Colors.fadeDuration, 0.0f);
// OnValidate can be called before OnEnable, this makes it unsafe to access other components
// since they might not have been initialized yet.
// OnSetProperty potentially access Animator or Graphics. (case 618186)
if (isActiveAndEnabled)
{
if (!interactable && EventSystem.current != null && EventSystem.current.currentSelectedGameObject == gameObject)
EventSystem.current.SetSelectedGameObject(null);
// Need to clear out the override image on the target...
DoSpriteSwap(null);
// If the transition mode got changed, we need to clear all the transitions, since we don't know what the old transition mode was.
StartColorTween(Color.white, true);
TriggerAnimation(m_AnimationTriggers.normalTrigger);
// And now go to the right state.
DoStateTransition(currentSelectionState, true);
}
}
protected override void Reset()
{
m_TargetGraphic = GetComponent();
}
#endif // if UNITY_EDITOR
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;
}
}
///
/// Clear any internal state from the Selectable (used when disabling).
///
protected virtual void InstantClearState()
{
string triggerName = m_AnimationTriggers.normalTrigger;
isPointerInside = false;
isPointerDown = false;
hasSelection = false;
switch (m_Transition)
{
case Transition.ColorTint:
StartColorTween(Color.white, true);
break;
case Transition.SpriteSwap:
DoSpriteSwap(null);
break;
case Transition.Animation:
TriggerAnimation(triggerName);
break;
}
}
///
/// Transition the Selectable to the entered state.
///
/// State to transition to
/// Should the transition occur instantly.
protected virtual void DoStateTransition(SelectionState state, bool instant)
{
if (!gameObject.activeInHierarchy)
return;
Color tintColor;
Sprite transitionSprite;
string triggerName;
switch (state)
{
case SelectionState.Normal:
tintColor = m_Colors.normalColor;
transitionSprite = null;
triggerName = m_AnimationTriggers.normalTrigger;
break;
case SelectionState.Highlighted:
tintColor = m_Colors.highlightedColor;
transitionSprite = m_SpriteState.highlightedSprite;
triggerName = m_AnimationTriggers.highlightedTrigger;
break;
case SelectionState.Pressed:
tintColor = m_Colors.pressedColor;
transitionSprite = m_SpriteState.pressedSprite;
triggerName = m_AnimationTriggers.pressedTrigger;
break;
case SelectionState.Selected:
tintColor = m_Colors.selectedColor;
transitionSprite = m_SpriteState.selectedSprite;
triggerName = m_AnimationTriggers.selectedTrigger;
break;
case SelectionState.Disabled:
tintColor = m_Colors.disabledColor;
transitionSprite = m_SpriteState.disabledSprite;
triggerName = m_AnimationTriggers.disabledTrigger;
break;
default:
tintColor = Color.black;
transitionSprite = null;
triggerName = string.Empty;
break;
}
switch (m_Transition)
{
case Transition.ColorTint:
StartColorTween(tintColor * m_Colors.colorMultiplier, instant);
break;
case Transition.SpriteSwap:
DoSpriteSwap(transitionSprite);
break;
case Transition.Animation:
TriggerAnimation(triggerName);
break;
}
}
///
/// An enumeration of selected states of objects
///
protected enum SelectionState
{
///
/// The UI object can be selected.
///
Normal,
///
/// The UI object is highlighted.
///
Highlighted,
///
/// The UI object is pressed.
///
Pressed,
///
/// The UI object is selected
///
Selected,
///
/// The UI object cannot be selected.
///
Disabled,
}
// Selection logic
///
/// Finds the selectable object next to this one.
///
///
/// The direction is determined by a Vector3 variable.
///
/// The direction in which to search for a neighbouring Selectable object.
/// The neighbouring Selectable object. Null if none found.
///
///
///
///
///
public Selectable 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 == Navigation.Mode.Vertical || m_Navigation.mode == Navigation.Mode.Horizontal);
Selectable bestPick = null;
Selectable bestFurthestPick = null;
for (int i = 0; i < s_SelectableCount; ++i)
{
Selectable sel = s_Selectables[i];
if (sel == this)
continue;
if (!sel.IsInteractable() || sel.navigation.mode == Navigation.Mode.None)
continue;
#if UNITY_EDITOR
// Apart from runtime use, FindSelectable is used by custom editors to
// draw arrows between different selectables. For scene view cameras,
// only selectables in the same stage should be considered.
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;
// Value that is the distance out along the direction.
float dot = Vector3.Dot(dir, myVector);
// If element is in wrong direction and we have wrapAround enabled check and cache it if furthest away.
if (wantsWrapAround && dot < 0)
{
score = -dot * myVector.sqrMagnitude;
if (score > maxFurthestScore)
{
maxFurthestScore = score;
bestFurthestPick = sel;
}
continue;
}
// Skip elements that are in the wrong direction or which have zero distance.
// This also ensures that the scoring formula below will not have a division by zero error.
if (dot <= 0)
continue;
// This scoring function has two priorities:
// - Score higher for positions that are closer.
// - Score higher for positions that are located in the right direction.
// This scoring function combines both of these criteria.
// It can be seen as this:
// Dot (dir, myVector.normalized) / myVector.magnitude
// The first part equals 1 if the direction of myVector is the same as dir, and 0 if it's orthogonal.
// The second part scores lower the greater the distance is by dividing by the distance.
// The formula below is equivalent but more optimized.
//
// If a given score is chosen, the positions that evaluate to that score will form a circle
// that touches pos and whose center is located along dir. A way to visualize the resulting functionality is this:
// From the position pos, blow up a circular balloon so it grows in the direction of dir.
// The first Selectable whose center the circular balloon touches is the one that's chosen.
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;
}
// Convenience function -- change the selection to the specified object if it's not null and happens to be active.
void Navigate(AxisEventData eventData, Selectable sel)
{
if (sel != null && sel.IsActive())
eventData.selectedObject = sel.gameObject;
}
///
/// Find the selectable object to the left of this one.
///
///
///
///
///
///
public virtual Selectable FindSelectableOnLeft()
{
if (m_Navigation.mode == Navigation.Mode.Explicit)
{
return m_Navigation.selectOnLeft;
}
if ((m_Navigation.mode & Navigation.Mode.Horizontal) != 0)
{
return FindSelectable(transform.rotation * Vector3.left);
}
return null;
}
///
/// Find the selectable object to the right of this one.
///
///
///
///
///
///
public virtual Selectable FindSelectableOnRight()
{
if (m_Navigation.mode == Navigation.Mode.Explicit)
{
return m_Navigation.selectOnRight;
}
if ((m_Navigation.mode & Navigation.Mode.Horizontal) != 0)
{
return FindSelectable(transform.rotation * Vector3.right);
}
return null;
}
///
/// The Selectable object above current
///
///
///
///
///
///
public virtual Selectable FindSelectableOnUp()
{
if (m_Navigation.mode == Navigation.Mode.Explicit)
{
return m_Navigation.selectOnUp;
}
if ((m_Navigation.mode & Navigation.Mode.Vertical) != 0)
{
return FindSelectable(transform.rotation * Vector3.up);
}
return null;
}
///
/// Find the selectable object below this one.
///
///
///
///
///
///
public virtual Selectable FindSelectableOnDown()
{
if (m_Navigation.mode == Navigation.Mode.Explicit)
{
return m_Navigation.selectOnDown;
}
if ((m_Navigation.mode & Navigation.Mode.Vertical) != 0)
{
return FindSelectable(transform.rotation * Vector3.down);
}
return null;
}
///
/// Determine in which of the 4 move directions the next selectable object should be found.
///
///
///
///
///
///
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 StartColorTween(Color targetColor, bool instant)
{
if (m_TargetGraphic == null)
return;
m_TargetGraphic.CrossFadeColor(targetColor, instant ? 0f : m_Colors.fadeDuration, true, true);
}
void DoSpriteSwap(Sprite newSprite)
{
if (image == null)
return;
image.overrideSprite = newSprite;
}
void TriggerAnimation(string triggername)
{
#if PACKAGE_ANIMATION
if (transition != Transition.Animation || animator == null || !animator.isActiveAndEnabled || !animator.hasBoundPlayables || string.IsNullOrEmpty(triggername))
return;
animator.ResetTrigger(m_AnimationTriggers.normalTrigger);
animator.ResetTrigger(m_AnimationTriggers.highlightedTrigger);
animator.ResetTrigger(m_AnimationTriggers.pressedTrigger);
animator.ResetTrigger(m_AnimationTriggers.selectedTrigger);
animator.ResetTrigger(m_AnimationTriggers.disabledTrigger);
animator.SetTrigger(triggername);
#endif
}
///
/// Returns whether the selectable is currently 'highlighted' or not.
///
///
/// Use this to check if the selectable UI element is currently highlighted.
///
///
///
/// UI and select from the list. Attach this script to the UI GameObject to see this script working. The script also works with non-UI elements, but highlighting works better with UI.
///
/// using UnityEngine;
/// using UnityEngine.Events;
/// using UnityEngine.EventSystems;
/// using UnityEngine.UI;
///
/// //Use the Selectable class as a base class to access the IsHighlighted method
/// public class Example : Selectable
/// {
/// //Use this to check what Events are happening
/// BaseEventData m_BaseEvent;
///
/// void Update()
/// {
/// //Check if the GameObject is being highlighted
/// if (IsHighlighted())
/// {
/// //Output that the GameObject was highlighted, or do something else
/// Debug.Log("Selectable is Highlighted");
/// }
/// }
/// }
/// ]]>
///
///
protected bool IsHighlighted()
{
if (!IsActive() || !IsInteractable())
return false;
return isPointerInside && !isPointerDown && !hasSelection;
}
///
/// Whether the current selectable is being pressed.
///
protected bool IsPressed()
{
if (!IsActive() || !IsInteractable())
return false;
return isPointerDown;
}
// Change the button to the correct state
private void EvaluateAndTransitionToSelectionState()
{
if (!IsActive() || !IsInteractable())
return;
DoStateTransition(currentSelectionState, false);
}
///
/// Evaluate current state and transition to pressed state.
///
///
///
///
///
///
public virtual void OnPointerDown(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
// Selection tracking
if (IsInteractable() && navigation.mode != Navigation.Mode.None && EventSystem.current != null)
EventSystem.current.SetSelectedGameObject(gameObject, eventData);
isPointerDown = true;
EvaluateAndTransitionToSelectionState();
}
///
/// Evaluate eventData and transition to appropriate state.
///
///
///
///
///
///
public virtual void OnPointerUp(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
isPointerDown = false;
EvaluateAndTransitionToSelectionState();
}
///
/// Evaluate current state and transition to appropriate state.
/// New state could be pressed or hover depending on pressed state.
///
///
///
///
///
///
public virtual void OnPointerEnter(PointerEventData eventData)
{
isPointerInside = true;
EvaluateAndTransitionToSelectionState();
}
///
/// Evaluate current state and transition to normal state.
///
///
///
///
///
///
public virtual void OnPointerExit(PointerEventData eventData)
{
isPointerInside = false;
EvaluateAndTransitionToSelectionState();
}
///
/// Set selection and transition to appropriate state.
///
///
///
///
///
///
public virtual void OnSelect(BaseEventData eventData)
{
hasSelection = true;
EvaluateAndTransitionToSelectionState();
}
///
/// Unset selection and transition to appropriate state.
///
///
///
///
///
///
public virtual void OnDeselect(BaseEventData eventData)
{
hasSelection = false;
EvaluateAndTransitionToSelectionState();
}
///
/// Selects this Selectable.
///
///
///
///
///
///
public virtual void Select()
{
if (EventSystem.current == null || EventSystem.current.alreadySelecting)
return;
EventSystem.current.SetSelectedGameObject(gameObject);
}
}
}