Button

首先给出 Button 的源码(此处使用的是 Unity 2021 当中的源码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class Button : Selectable, IPointerClickHandler, ISubmitHandler
{
[Serializable]
// 按钮的点击事件
public class ButtonClickedEvent : UnityEvent {}

[FormerlySerializedAs("onClick")]
[SerializeField]
// 保存所有按钮点击事件的回调
private ButtonClickedEvent m_OnClick = new ButtonClickedEvent();

protected Button()
{
}

// 即我们平时监听的 onClick.AddListener()
public ButtonClickedEvent onClick
{
get { return m_OnClick; }
set { m_OnClick = value; }
}

// 在按钮处于 活跃 且 可交互 状态时才触发回调事件
private void Press()
{
if (!IsActive() || !IsInteractable())
return;
UISystemProfilerApi.AddMarker("Button.onClick", this);
m_OnClick.Invoke();
}

// 鼠标点击时调用此方法,实现自 IPointerClickHandler 接口
public virtual void OnPointerClick(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
Press();
}

// 按下 “提交” 键触发,实现自 ISubmitHandler 接口
public virtual void OnSubmit(BaseEventData eventData)
{
Press();
if (!IsActive() || !IsInteractable())
return;
DoStateTransition(SelectionState.Pressed, false);
StartCoroutine(OnFinishSubmit());
}

private IEnumerator OnFinishSubmit()
{
var fadeTime = colors.fadeDuration;
var elapsedTime = 0f;
while (elapsedTime < fadeTime)
{
elapsedTime += Time.unscaledDeltaTime;
yield return null;
}
DoStateTransition(currentSelectionState, false);
}
}

大体逻辑如代码注释所示。此处 Button 实现了 IPointerClickHandler 接口的 OnPointerClick 方法。那么此处我们就去找寻一下调用链,观察这个方法到底是由谁来调用的。

调用链

查看引用可知,是 ExecuteEvents 类的 Excute 方法(此方法有多个重载,提供了许多通用的事件处理方法)来调用的。

1
2
3
4
5
private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}

大体的类关系如图所示,ExcuteEvents 当中的 Excute 方法会在鼠标松开时通过 Excute(和前者不是一个方法)去调用 OnPointerClickOnSubmit 方法。因而就触发了 Button 的 onClick 事件。

ExcuteEvents 当中,还定义了一个 EventFunction<T1> 的泛型委托,用于定义通用的事件处理方法,接着还定义了一个 pointerClickHandler 的属性,返回 s_PointerClickHandler 字段,也就是我们上面定义的存储处理点击的 Excute 方法的委托。

1
2
3
4
5
public delegate void EventFunction<T1>(T1 handler, BaseEventData eventData);
public static EventFunction<IPointerClickHandler> pointerClickHandler
{
get { return s_PointerClickHandler; }
}

接着我们该查看这个 pointerClickHandler 是由谁来调用的:

  • StandaloneInputModule.ReleaseMouse
  • StandaloneInputModule.ProcessTouchPress
  • TouchInputModule.ProcessTouchPress

此处的 StandaloneInputModuleTouchInputModule 都是继承自 BaseInputModule。其主要是用于处理鼠标,键盘,控制器等设备的输入。

此时我们可以自顶向下来看:EventSystem 类,也就是我们新建 UI 会自动生成的那个组件。其的 Update 方法中会每帧更新检查可用的输入模块的状态是否变化。也就是其中的 ChangeEventModule。并且会每帧调用 TickModules(用来更新每个模块的状态),最后会调用当前每个模块( m_CurrentInputModule )的 Process 方法。

TouchInputModule 当中:

入口 Process(),调用 ProcessTouchEvents(),接着调用 ProcessTouchPress() 去调用到了 pointerClickHandler

1
2
3
4
5
6
7
8
9
10
/
protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
{
// ...
if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
{
ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
}
// ...
}

StandaloneInputModule 当中:’

入口 Process(),调用 ProcessTouchEvents(),接着调用 ProcessTouchPress(),其中的函数签名和调用逻辑也是和上面的一模一样。

1
2
3
4
5
6
7
8
9
protected void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released)
{
// ...
if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
{
ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
}
// ...
}

同时在 ProcessTouchEvents() 也会调用 ProcessMouseEvent(),接着调用其自身重载然后调用 ProcessMousePress()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 计算和处理任何鼠标按钮状态的变化
protected void ProcessMousePress(MouseButtonEventData data)
{
// ...
// 鼠标按键抬起时调用(包括鼠标左键,右键,滚轮)
if (data.ReleasedThisFrame())
{
ReleaseMouse(pointerEvent, currentOverGo);
}
}

// 满足松开鼠标条件时调用
// currentOverGo: 当前选中的游戏物体
private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
{
// ...
if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
{
ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
}
// ...
}

此时所有的线索都指向了 Excute 的一个版本的重载,我们直接看看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// target: 需要执行事件的游戏对象
public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
{
var internalHandlers = ListPool<IEventSystemHandler>.Get();
// 获取 target 对象的事件
GetEventList<T>(target, internalHandlers);
// if (s_InternalHandlers.Count > 0)
// Debug.Log("Executinng " + typeof (T) + " on " + target);
var internalHandlersCount = internalHandlers.Count;
for (var i = 0; i < internalHandlersCount; i++)
{
T arg;
try
{
arg = (T)internalHandlers[i];
}
catch (Exception e)
{
var temp = internalHandlers[i];
Debug.LogException(new Exception(string.Format("Type {0} expected {1} received.", typeof(T).Name, temp.GetType().Name), e));
continue;
}
try
{
// 执行 EventFunction<T> 委托,比如 pointerClickHandler(arg, eventData)
functor(arg, eventData);
}
catch (Exception e)
{
Debug.LogException(e);
}
}

var handlerCount = internalHandlers.Count;
ListPool<IEventSystemHandler>.Release(internalHandlers);
return handlerCount > 0;
}

总结一下:EventSystem 会在 Update() 当中每帧调用当前可用的 BaseInputModuleProcess() 方法。其主要是用来处理鼠标的按下,抬起等事件。比如当鼠标抬起会调用 ReleaseMouse() 方法,并最终调用 Excute() 方法触发 IPointerClick 接口的 OnPointerClick() 方法。

小问题:ReleaseMouse() 只有抬起鼠标左键才会触发吗?

如果单纯说触发,左键,右键以及滚轮都可以触发 ReleaseMouse() 方法,但是在 ButtonOnPointerClick() 实现当中,筛掉了非鼠标左键,因此只有鼠标左键才能触发 Button 的点击事件。

1
2
3
4
5
6
7
public virtual void OnPointerClick(PointerEventData eventData)
{
// 筛掉非鼠标左键
if (eventData.button != PointerEventData.InputButton.Left)
return;
Press();
}

以上就是对于 Button 源码的简单拆解。


参考: