按照惯例,附上UGUI源码下载地址。
在Selectable之前,我们看到,它被添加了四个属性:
[AddComponentMenu("UI/Selectable", 70)]
[ExecuteInEditMode]
[SelectionBase]
[DisallowMultipleComponent]
ExecuteInEditMode表示该组件会在编辑器下执行。
SelectionBase标记该对象为选择基本对象。
DisallowMultipleComponent不允许一个GameObject上出现同种组件。我们无法为一个对象添加两个Selectable组件,也无法为一个Button对象再添加一个InputField组件。
Selectable继承自UIBehaviour,并且继承了7个接口。
public class Selectable
:
UIBehaviour,
IMoveHandler,
IPointerDownHandler, IPointerUpHandler,
IPointerEnterHandler, IPointerExitHandler,
ISelectHandler, IDeselectHandler
Selectable的Awake获取了一个Graphic的组件实例m_TargetGraphic(Image组件间接继承自Graphic)。如果我们选择的Transition为Color Tint,当Selectable的状态变化(普通、高亮、按压、禁用)时,会调用m_TargetGraphic的CrossFadeColor方法,即将当前图像渐变为指定颜色。
OnEnable(被调用的时机参见Untiy3D组件小贴士(一)OnEnabled与OnDisabled)里,会将该实例添加至Selectable的静态列表s_List里(s_List里存放了所有可用的Selectable,用于UI导航的查询),然后设置实例的状态为Normal或者Highlighted(亦或Disabled)。
OnDisable(被调用的时机参见Untiy3D组件小贴士(一)OnEnabled与OnDisabled)里,将实例清除状态(需要恢复颜色和图片,以及播放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;
}
上述的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;
}
}
}
最后讲解一下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派生出新的自定义组件。