UGUI使用教程(四)Selectable与Button

发表于2018-01-27
评论0 3.2k浏览
Selectable是UGUI的核心组件,除了最常用的Button,它还是Scrollbar、Dropdown、Slider、Toggle、InputField这些组件的基类。在本篇文章中将和大家分析Selectable和Button的源码,并介绍Selectable与Button的实现原理。

按照惯例,附上UGUI源码下载地址

在Selectable之前,我们看到,它被添加了四个属性:
[AddComponentMenu("UI/Selectable", 70)]  
[ExecuteInEditMode]  
[SelectionBase]  
[DisallowMultipleComponent]  

AddComponentMenu是为Component菜单添加选项,顺序为70。当点击一个GameObject的AddComponent按钮时,会弹出一个菜单,点击UI选项,就可以看到排在最后一个的就是Selectable。
ExecuteInEditMode表示该组件会在编辑器下执行。
SelectionBase标记该对象为选择基本对象。
DisallowMultipleComponent不允许一个GameObject上出现同种组件。我们无法为一个对象添加两个Selectable组件,也无法为一个Button对象再添加一个InputField组件。

Selectable继承自UIBehaviour,并且继承了7个接口。
public class Selectable  
    :  
    UIBehaviour,  
    IMoveHandler,  
    IPointerDownHandler, IPointerUpHandler,  
    IPointerEnterHandler, IPointerExitHandler,  
    ISelectHandler, IDeselectHandler  

UIBehaviour是所有UI组件的基类,它被放在EventSystem目录下,除了IsDestroyed以外,其他都是虚函数,我们也可以认为它是事件系统的一部分,它添加了Awake(当脚本实例被加载,即AddComponent时)、OnRectTransformDimensionsChange(当RectTransform维度变化)等方法来接收来自UnityEngine的事件。

Selectable的Awake获取了一个Graphic的组件实例m_TargetGraphic(Image组件间接继承自Graphic)。如果我们选择的Transition为Color Tint,当Selectable的状态变化(普通、高亮、按压、禁用)时,会调用m_TargetGraphic的CrossFadeColor方法,即将当前图像渐变为指定颜色。

OnEnable里,会将该实例添加至Selectable的静态列表s_List里(s_List里存放了所有可用的Selectable,用于UI导航的查询),然后设置实例的状态为Normal或者Highlighted(亦或Disabled)。

OnDisable里,将实例清除状态(需要恢复颜色和图片,以及播放normal动画),并从s_List中将该实例移除。

OnDidApplyAnimationProperties(当应用动画属性后)里,会通过OnSetProperty方法调用InternalEvaluateAndTransitionToSelectionState,刷新当前状态。

OnCanvasGroupChanged(当CanvasGroup变化时)里,会判断新的GanvasGroup的interactable,如果GanvasGroup的interactable为false,那么Selectable本身也就被禁用了。接着,刷新当前状态。

然后我们看Selectable继承的几个接口,可以先看一下《UGUI使用教程(三)输入模块》了解这些接口的触发时机。

继承自IMoveHandler需要实现OnMove方法。根据移动方向,导航到下一个Selectable组件。
继承自IPointerDownHandler需要实现OnPointerDown方法。调用EventSystem.current.SetSelectedGameObject将自己设为当前选中对象(会调用自己的OnSelect和前任对象的OnDeselect),标记isPointerDown为true并刷新状态(当isPointerInside和isPointerDown同事为true的时候为Pressed状态)。
继承自IPointerUpHandler需要实现OnPointerUp方法。标记isPointerDown为false并刷新状态。
继承自IPointerEnterHandler需要实现OnPointerEnter方法。标记isPointerInside为true并刷新状态。
继承自IPointerExitHandler需要实现OnPointerExit方法。标记isPointerInside为false并刷新状态。
继承自ISelectHandler需要实现OnSelect方法。标记hasSelection为true并刷新状态(hasSelection为true时为Highlighted状态,另外isPointerInside和isPointerDown也是判断Highlighted状态的依据,稍后会提到)。
继承自IDeselectHandler需要实现OnDeselect方法。标记hasSelection为false并刷新状态。

IsHighlighted方法:
protected bool IsHighlighted(BaseEventData eventData)  
{  
    if (!IsActive())  
        return false;  
    if (IsPressed())  
        return false;  
    bool selected = hasSelection;  
    if (eventData is PointerEventData)  
    {  
        var pointerData = eventData as PointerEventData;  
        selected |=  
            (isPointerDown && !isPointerInside && pointerData.pointerPress == gameObject) // This object pressed, but pointer moved off  
            || (!isPointerDown && isPointerInside && pointerData.pointerPress == gameObject) // This object pressed, but pointer released over (PointerUp event)  
            || (!isPointerDown && isPointerInside && pointerData.pointerPress == null); // Nothing pressed, but pointer is over  
    }  
    else  
    {  
        selected |= isPointerInside;  
    }  
    return selected;  
}  

参数为输入模块传递过来的事件数据,主要用于判断事件响应对象是否是本对象(或者是null)。

上述的OnPointerDown等方法是通过EvaluateAndTransitionToSelectionState方法评估并刷新状态。在这个方法里,UpdateSelectionState调用IsPressed和IsHighlighted判断当前状态。InternalEvaluateAndTransitionToSelectionState判断当前组件是否被禁用,然后调用DoStateTransition方法。
protected virtual void DoStateTransition(SelectionState state, bool instant)  
{  
    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.Disabled:  
            tintColor = m_Colors.disabledColor;  
            transitionSprite = m_SpriteState.disabledSprite;  
            triggerName = m_AnimationTriggers.disabledTrigger;  
            break;  
        default:  
            tintColor = Color.black;  
            transitionSprite = null;  
            triggerName = string.Empty;  
            break;  
    }  
    if (gameObject.activeInHierarchy)  
    {  
        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;  
        }  
    }  
}  

根据状态设置颜色、图片或动画名称,再通过StartColorTween、DoSpriteSwap或TriggerAnimation方法在UI上体现出状态的改变。

最后介绍下Button组件。Button继承自Selectable并额外继承了IPointerClickHandler、ISubmitHandler两个接口。它还添加了一个UnityEvent类型的事件onClick。onClick事件可以添加用户自定义的监听,具体方法可以通过编辑器添加也可以通过onClick.AddListener添加。
OnPointerClick会调用Press方法来回调onClick。
OnSubmit也会调用Press方法,并切换状态为Pressed,并开启协程调用OnFinishSubmit,让状态渐变为当前状态(通过UpdateSelectionState获得的状态)。

可以看出Button就是相对于Selectable添加了响应点击和确认事件的接口,并且开放了可添加用户自定义监听的onClick事件。

而Selectable的作用在于提供了基于鼠标事件的四种状态变化。一方面,为Button、Dropdown等派生类提供了基础的逻辑,另一方面,我们也可以根据Selectable派生出新的自定义组件。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论