从0到1的UGUI学习-02简单拆解Button源码
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
61public class Button : Selectable, IPointerClickHandler, ISubmitHandler
{
[ ]
// 按钮的点击事件
public class ButtonClickedEvent : UnityEvent {}
[ ]
[ ]
// 保存所有按钮点击事件的回调
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
5private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}
大体的类关系如图所示,ExcuteEvents
当中的 Excute
方法会在鼠标松开时通过 Excute
(和前者不是一个方法)去调用 OnPointerClick
和 OnSubmit
方法。因而就触发了 Button 的 onClick
事件。
在 ExcuteEvents
当中,还定义了一个 EventFunction<T1>
的泛型委托,用于定义通用的事件处理方法,接着还定义了一个 pointerClickHandler
的属性,返回 s_PointerClickHandler
字段,也就是我们上面定义的存储处理点击的 Excute
方法的委托。1
2
3
4
5public delegate void EventFunction<T1>(T1 handler, BaseEventData eventData);
public static EventFunction<IPointerClickHandler> pointerClickHandler
{
get { return s_PointerClickHandler; }
}
接着我们该查看这个 pointerClickHandler
是由谁来调用的:
StandaloneInputModule.ReleaseMouse
StandaloneInputModule.ProcessTouchPress
TouchInputModule.ProcessTouchPress
此处的 StandaloneInputModule
和 TouchInputModule
都是继承自 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
9protected 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()
当中每帧调用当前可用的 BaseInputModule
的 Process()
方法。其主要是用来处理鼠标的按下,抬起等事件。比如当鼠标抬起会调用 ReleaseMouse()
方法,并最终调用 Excute()
方法触发 IPointerClick
接口的 OnPointerClick()
方法。
小问题:ReleaseMouse()
只有抬起鼠标左键才会触发吗?
如果单纯说触发,左键,右键以及滚轮都可以触发 ReleaseMouse()
方法,但是在 Button
的 OnPointerClick()
实现当中,筛掉了非鼠标左键,因此只有鼠标左键才能触发 Button
的点击事件。1
2
3
4
5
6
7public virtual void OnPointerClick(PointerEventData eventData)
{
// 筛掉非鼠标左键
if (eventData.button != PointerEventData.InputButton.Left)
return;
Press();
}
以上就是对于 Button 源码的简单拆解。
参考: