UIGU源码分析4:Selectable

源码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();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iAs8sTVT-1646484140613)(D:\UnityProjectSpace\BlogRecord\UGUI源码分析\Image\image-20220302232020097.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3PsQGxad-1646484140614)(D:\UnityProjectSpace\BlogRecord\UGUI源码分析\Image\image-20220302232049679.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vX2Ys5hW-1646484140614)(D:\UnityProjectSpace\BlogRecord\UGUI源码分析\Image\image-20220302232109743.png)]


具体的过度类型 在不同状态时的响应表现就是有下面函数确定

    /// <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进行显示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VwmfJsca-1646484140615)(D:\UnityProjectSpace\BlogRecord\UGUI源码分析\Image\image-20220303231024506.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mThkaoOl-1646484140615)(D:\UnityProjectSpace\BlogRecord\UGUI源码分析\Image\image-20220303231216988.png)]
在这里插入图片描述

先简单看下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中出现了配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ocsAoIU-1646484140616)(D:\UnityProjectSpace\BlogRecord\UGUI源码分析\Image\image-20220303233508732.png)]

对应的代码

    [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);
    }

猜你喜欢

转载自blog.csdn.net/NippyLi/article/details/123300754