从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 源码的简单拆解。
参考:








