源码4:Selectable
前面以及讲完了UGUI的输入模块和事件系统,下面要分析一下UGUI的相关控件。对于UI画布上的可视化元素,包括Image/RawImage/Text,存在一个共同的基类Graphic。对于事件系统,每一个可执行操作的元素,例如Button/Toggle等,也存在一个基类Selectable。 本文会谈谈Selectable的实现
Selectable
简单看下Selectable的定义
/// <summary>
/// Simple selectable object - derived from to create a selectable control.
/// </summary>
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;
/// <summary>
/// Copy of the array of all the selectable objects currently active in the scene.
/// </summary>
/// <example>
/// <code>
/// using UnityEngine;
/// using System.Collections;
/// using UnityEngine.UI; // required when using UI elements in scripts
///
/// public class Example : MonoBehaviour
/// {
/// //Displays the names of all selectable elements in the scene
/// public void GetNames()
/// {
/// foreach (Selectable selectableUI in Selectable.allSelectablesArray)
/// {
/// Debug.Log(selectableUI.name);
/// }
/// }
/// }
/// </code>
/// </example>
....
}
可以看到Selectable 继承了很多的事件接口。包括移动 按下,抬起 鼠标进入 鼠标出来 选中和取消选中
在这里面还定义了许多通用的字段属性
Transition 过度相关
Selectable状态改变时 的具体执行的过度类型
/// <summary>
///Transition mode for a Selectable.
/// </summary>
public enum Transition
{
/// <summary>
/// No Transition.
/// </summary>
None,
/// <summary>
/// Use an color tint transition.
/// </summary>
ColorTint,
/// <summary>
/// Use a sprite swap transition.
/// </summary>
SpriteSwap,
/// <summary>
/// Use an animation transition.
/// </summary>
Animation
}
// Type of the transition that occurs when the button state changes.
[FormerlySerializedAs("transition")]
[SerializeField]
private Transition m_Transition = Transition.ColorTint;
Selectable 的状态枚举
protected enum SelectionState
{
/// <summary>
/// The UI object can be selected.
/// </summary>
Normal,
/// <summary>
/// The UI object is highlighted.
/// </summary>
Highlighted,
/// <summary>
/// The UI object is pressed.
/// </summary>
Pressed,
/// <summary>
/// The UI object is selected
/// </summary>
Selected,
/// <summary>
/// The UI object cannot be selected.
/// </summary>
Disabled,
}
Normal : 正常状态,未被选中 通常游戏刚启动的都是这个状态
Highlighted 高亮状态 通常鼠标进入这个Selectable范围中(Pointer Enter)的时候
Pressed 按下状态
Selected 选择状态 但按Selectable压过后 松开鼠标或手指而且没有在点击任何UI元素 这个时候就是选择状态
Disable 当被禁用的时候
四种过度类型(Transition ) 都包含上面5种状态 每种类型具体表现就由下面参数进行配置
//默认ColorTint类型
[FormerlySerializedAs("transition")]
[SerializeField]
private Transition m_Transition = Transition.ColorTint;
//ColorTint类型 时对应的参数配置(主要是配置五种状态颜色)
// Colors used for a color tint-based transition.
[FormerlySerializedAs("colors")]
[SerializeField]
private ColorBlock m_Colors = ColorBlock.defaultColorBlock;
//SpriteSwap类型 时对应的参数配置(主要是配置五种Sprite)
// Sprites used for a Image swap-based transition.
[FormerlySerializedAs("spriteState")]
[SerializeField]
private SpriteState m_SpriteState;
//Animation类型 时对应的参数配置(主要是配置五种AnimationClip)
[FormerlySerializedAs("animationTriggers")]
[SerializeField]
private AnimationTriggers m_AnimationTriggers = new AnimationTriggers();
具体的过度类型 在不同状态时的响应表现就是有下面函数确定
/// <summary>
/// Transition the Selectable to the entered state.
/// </summary>
/// <param name="state">State to transition to</param>
/// <param name="instant">Should the transition occur instantly.</param>
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:
//处理animation
TriggerAnimation(triggerName);
break;
}
}
当前的SelectionState 由下面函数确定
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;
}
}
IsInteractable()判断当前Selectable是否是可交互(禁用)状态
public virtual bool IsInteractable()
{
return m_GroupsAllowInteraction && m_Interactable;
}
isPointerDown 通常只有在响应了OnPointerDown 事件时才是True
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();
}
hasSelection 通常响应了OnSelect 事件时才是true(具体时操作可以参考之前讲解的输入模块)
public virtual void OnSelect(BaseEventData eventData)
{
hasSelection = true;
EvaluateAndTransitionToSelectionState();
}
isPointerInside 通常响应了OnPointerEnter事件时才是true(具体时操作可以参考之前讲解的输入模块)
public virtual void OnPointerEnter(PointerEventData eventData)
{
isPointerInside = true;
EvaluateAndTransitionToSelectionState();
}
当进行各个操作后 会调用调用EvaluateAndTransitionToSelectionState 来执行具体的状态变化的表现
// Change the button to the correct state
private void EvaluateAndTransitionToSelectionState()
{
if (!IsActive() || !IsInteractable())
return;
DoStateTransition(currentSelectionState, false);
}
所有的状态在Disable的时候会进行重置
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;
}
}
Navigation
Navigation顾名思义导航的意思,当我们在用方向键操作的时候 ,可以通过上下左右 可以从从一个Selectable移动到下一个Selectable。
例如下面截图 中间的线是导航方向,点击继承于Selectable 的控件上的Visualize进行显示
先简单看下Navigation中的一些定义
public enum Mode
{
//不能切换到下一个按钮
/// <summary>
/// No navigation is allowed from this object.
/// </summary>
None = 0,
//只能水平方向上的进行切换Selectable
/// <summary>
/// Horizontal Navigation.
/// </summary>
/// <remarks>
/// Navigation should only be allowed when left / right move events happen.
/// </remarks>
Horizontal = 1,
//只能竖直方向上的进行切换Selectable
/// <summary>
/// Vertical navigation.
/// </summary>
/// <remarks>
/// Navigation should only be allowed when up / down move events happen.
/// </remarks>
Vertical = 2,
//方向无限制
/// <summary>
/// Automatic navigation.
/// </summary>
/// <remarks>
/// Attempt to find the 'best' next object to select. This should be based on a sensible heuristic.
/// </remarks>
Automatic = 3,
//应该对当前按钮的四个方向分别专门指定一个Selectable。当发生切换的时候,直接跳转至对应Selectable
/// <summary>
/// Explicit navigation.
/// </summary>
/// <remarks>
/// User should explicitly specify what is selected by each move event.
/// </remarks>
Explicit = 4,
}
Explicit 有点特殊当我们选择Explicit 的时候可以看到在Unity中出现了配置
对应的代码
[Tooltip("Enables navigation to wrap around from last to first or first to last element. Does not work for automatic grid navigation")]
[SerializeField]
private bool m_WrapAround;
// Game object selected when the joystick moves up. Used when navigation is set to "Explicit".
[SerializeField]
private Selectable m_SelectOnUp;
// Game object selected when the joystick moves down. Used when navigation is set to "Explicit".
[SerializeField]
private Selectable m_SelectOnDown;
// Game object selected when the joystick moves left. Used when navigation is set to "Explicit".
[SerializeField]
private Selectable m_SelectOnLeft;
// Game object selected when the joystick moves right. Used when navigation is set to "Explicit".
[SerializeField]
private Selectable m_SelectOnRight;
Navigation的处理其实就是在我们上面的StandaloneInputModule模块中
public override void Process()
{
if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
return;
bool usedEvent = SendUpdateEventToSelectedObject();
// case 1004066 - touch / mouse events should be processed before navigation events in case
// they change the current selected gameobject and the submit button is a touch / mouse button.
// touch needs to take precedence because of the mouse emulation layer
if (!ProcessTouchEvents() && input.mousePresent)
ProcessMouseEvent();
if (eventSystem.sendNavigationEvents)
{
if (!usedEvent)
usedEvent |= SendMoveEventToSelectedObject();
if (!usedEvent)
SendSubmitEventToSelectedObject();
}
}
这里面SendMoveEventToSelectedObject 和SendSubmitEventToSelectedObject 就是处理上下左后 和submit Cancel事件
/// <summary>
/// Calculate and send a move event to the current selected object.
/// </summary>
/// <returns>If the move event was used by the selected object.</returns>
protected bool SendMoveEventToSelectedObject()
{
float time = Time.unscaledTime;
//根据输入轴获取原始的向量(上下左右)
Vector2 movement = GetRawMoveVector();
//x y方向移动近似都为0 相当于没有移动 直接返回
if (Mathf.Approximately(movement.x, 0f) && Mathf.Approximately(movement.y, 0f))
{
m_ConsecutiveMoveCount = 0;
return false;
}
//判断移动方向是否相似
bool similarDir = (Vector2.Dot(movement, m_LastMoveVector) > 0);
//这里做一个延时???????????没看懂 后面再复查
// If direction didn't change at least 90 degrees, wait for delay before allowing consequtive event.
if (similarDir && m_ConsecutiveMoveCount == 1)
{
if (time <= m_PrevActionTime + m_RepeatDelay)
return false;
}
// If direction changed at least 90 degree, or we already had the delay, repeat at repeat rate.
else
{
if (time <= m_PrevActionTime + 1f / m_InputActionsPerSecond)
return false;
}
//------------------------------------------
//根据输入值获取轴数据(确定上下左右的方向)
var axisEventData = GetAxisEventData(movement.x, movement.y, 0.6f);
if (axisEventData.moveDir != MoveDirection.None)
{
//发送移动事件给选中对象(Selectable对象)
ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, axisEventData, ExecuteEvents.moveHandler);
if (!similarDir)
m_ConsecutiveMoveCount = 0;
m_ConsecutiveMoveCount++;
m_PrevActionTime = time;
m_LastMoveVector = movement;
}
else
{
m_ConsecutiveMoveCount = 0;
}
return axisEventData.used;
}
事件的接收处理就是在Selectable中处理了 根据轴向的不同获取对应的选中对象。
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;
}
// 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;
}
eventData.selectedObject = sel.gameObject; 在进行调用的时候实际上是调用到了EventSystem 中的SetSelectedGameObject 方法 发送选中/取消选中事件
public void SetSelectedGameObject(GameObject selected, BaseEventData pointer)
{
if (m_SelectionGuard)
{
Debug.LogError("Attempting to select " + selected + "while already selecting an object.");
return;
}
m_SelectionGuard = true;
if (selected == m_CurrentSelected)
{
m_SelectionGuard = false;
return;
}
// Debug.Log("Selection: new (" + selected + ") old (" + m_CurrentSelected + ")");
ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.deselectHandler);
m_CurrentSelected = selected;
ExecuteEvents.Execute(m_CurrentSelected, pointer, ExecuteEvents.selectHandler);
m_SelectionGuard = false;
}
private BaseEventData m_DummyData;
private BaseEventData baseEventDataCache
{
get
{
if (m_DummyData == null)
m_DummyData = new BaseEventData(this);
return m_DummyData;
}
}
FindSelectableOnRight四个函数 就是用来查找当前Selectable对应方向的下一个Selectable。当查找到后,就根据方向的不同,就会导航至对应方向的下一个Selectable并进行选中,选中的的时候触发选中事件,进行状态改变(上面讲解的状态)
public virtual void OnSelect(BaseEventData eventData)
{
hasSelection = true;
EvaluateAndTransitionToSelectionState();
}
private void EvaluateAndTransitionToSelectionState()
{
if (!IsActive() || !IsInteractable())
return;
DoStateTransition(currentSelectionState, false);
}