Unity UGUI Button事件流程

一、场景结构

在这里插入图片描述

二、测试代码

public class TestBtn : MonoBehaviour
{
     
     void Start()
     {
         var btn = GetComponent<Button>();
         btn.onClick.AddListener(OnClick);
     }

     private void OnClick()
     {
         Debug.Log("666");
     }
 }

1.当添加事件时

// 实例化一个ButtonClickedEvent的事件
[FormerlySerializedAs("onClick")]
[SerializeField]
private ButtonClickedEvent m_OnClick = new ButtonClickedEvent();

//常用的onClick.AddListener()就是监听这个事件
public ButtonClickedEvent onClick
{
	  get { return m_OnClick; }
	  set { m_OnClick = value; }
}

//Button.cs部分源码

2.当按钮点击时

 //如果按钮处于活跃状态并且可交互(Interactable设置为true),则触发事件
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();
 }
//Button.cs部分源码

3.是如何执行的

private static readonly EventFunction<IPointerClickHandler> s_PointerClickHandler = Execute;

public static EventFunction<IPointerClickHandler> pointerClickHandler
{
    get { return s_PointerClickHandler; }
}

//调用关键代码
private static void Execute(IPointerClickHandler handler, BaseEventData eventData)
{
    handler.OnPointerClick(ValidateEventData<PointerEventData>(eventData));
}

//ExecuteEvents.cs部分源码
  • 实际调用了目标对象(如Button)的 OnPointerClick() 方法。
StandaloneInputModule.ReleaseMouse

继续跟踪源码在StandaloneInputModule

private void ReleaseMouse(PointerEventData pointerEvent, GameObject currentOverGo)
{
    // 执行 PointerUp 事件处理
    ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

    // 从currentOverGo到Parent查找实现了IPointerClickHandler的物体
    var pointerClickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

    //是否符合条件
    if (pointerEvent.pointerClick == pointerClickHandler && pointerEvent.eligibleForClick)
    {
        // 最终执行点击事件
        ExecuteEvents.Execute(pointerEvent.pointerClick, pointerEvent, ExecuteEvents.pointerClickHandler);
    }
	//....省略其他代码...
}
 //StandaloneInputModule.cs部分源码

ExecuteEvents.GetEventHandler方法

public static GameObject GetEventHandler<T>(GameObject root) where T : IEventSystemHandler
{
    if (root == null)
        return null;

    Transform t = root.transform;
    while (t != null)
    {
        if (CanHandleEvent<T>(t.gameObject))
            return t.gameObject;
        t = t.parent;
    }
    return null;
}

关键点:

  • pointerEvent.eligibleForClick 表示这次点击是否满足触发条件
  • 获取当前鼠标释放时的 GameObject (currentOverGo)。
  • currentOverGo其实是场景里面的Text所在Gameobject,(是如何得到的,后面会写到)
  • 但因为Text没有实现IPointerClickHandler方法,会向父节点查找,最后找到了Button组件
  • 最后通过 ExecuteEvents.Execute 调用 OnPointerClick 方法。

ReleaseMouseUpdateModule中调用

StandaloneInputModule.UpdateModule
public override void UpdateModule()
 {
     if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
     {
         if (m_InputPointerEvent != null && m_InputPointerEvent.pointerDrag != null && m_InputPointerEvent.dragging)
         {
         	//关键代码,处理鼠标释放事件
             ReleaseMouse(m_InputPointerEvent, m_InputPointerEvent.pointerCurrentRaycast.gameObject);
         }

         m_InputPointerEvent = null;

         return;
     }

     m_LastMousePosition = m_MousePosition;
     m_MousePosition = input.mousePosition;
 }
  //StandaloneInputModule.cs部分源码
  • 主要用于记录鼠标位置。
  • 如果窗口失焦并且正在拖拽,则调用 ReleaseMouse() 来释放鼠标状态。

UpdateModuleBaseInputModule 类的方法

 public abstract class BaseInputModule : UIBehaviour{
        public virtual void UpdateModule(){}
 }

UpdateModule被在EventSystem中的TickModules方法调用

EventSystem.TickModules
private List<BaseInputModule> m_SystemInputModules = new List<BaseInputModule>();

 private void TickModules()
 {
     var systemInputModulesCount = m_SystemInputModules.Count;
     for (var i = 0; i < systemInputModulesCount; i++)
     {
         if (m_SystemInputModules[i] != null)
             m_SystemInputModules[i].UpdateModule();
     }
 }
//EventSystem.cs部分源码

TickModules方法被在EventSystem里的Update调用

EventSystem.Update
 protected virtual void Update()
 {
       if (current != this)
           return;
       TickModules();

       bool changedModule = false;
       
       //m_CurrentInputModule 就是场景里面的StandaloneInputModule组件
       
       //判断当前m_CurrentInputModule 有没有被更改或者为空的情况
       
       if (!changedModule && m_CurrentInputModule != null)
       		//处理鼠标事件
           m_CurrentInputModule.Process();

		//...省略其他代码...
}

  

EventSystemUpdate继承UIBehaviour

public class EventSystem : UIBehaviour
  • EventSystem 是一个继承自 UIBehaviour 的组件,必须挂载在场景中的某个 GameObject 上。

  • 它每帧调用自身的 Update() 方法(由 Unity 引擎自动调用):
    在这里插入图片描述

  • 所以,如果在按下鼠标后,EventSystem每帧都会检测何时释放鼠标,然后触发点击事件。

如果有的话触发点击事件,那么是如何知道要触发的哪个Button的点击事件的呢


4.得到点击的物体

回到EventSystemUpdate方法中

 protected virtual void Update()
 {
       if (current != this)
           return;
       TickModules();

       bool changedModule = false;
       
       //m_CurrentInputModule 就是场景里面的StandaloneInputModule组件
       
       //判断当前m_CurrentInputModule 有没有被更改或者为空的情况
       
       if (!changedModule && m_CurrentInputModule != null)
       		//处理鼠标事件
           m_CurrentInputModule.Process();

		//...省略其他代码...
}
  • 这里会调用所有注册的 BaseInputModule 子类的 UpdateModule() 方法。
  • 其中就包括 StandaloneInputModule

可以看到,如果没有变更输入模块并且当前输入模块不为空,会在每帧执行Process方法

 public abstract class BaseInputModule : UIBehaviour
 {
   public abstract void Process();
 }

而场景里面的StandaloneInputModule 继承了PointerInputModule

public class StandaloneInputModule : PointerInputModule

PointerInputModule实现了BaseInputModule接口

public abstract class PointerInputModule : BaseInputModule

StandaloneInputModule 重写了Process方法

 public override void Process()
 {
       if (!eventSystem.isFocused && ShouldIgnoreEventsOnNoFocus())
           return;

       bool usedEvent = SendUpdateEventToSelectedObject();

       // 案例 1004066 - 在处理导航事件之前应先处理触摸/鼠标事件,
       // 因为它们可能会改变当前选中的游戏对象,并且提交按钮可能是触摸/鼠标按钮。
       // 由于存在鼠标模拟层,触摸需要优先处理。
       if (!ProcessTouchEvents() && input.mousePresent)
       	    //处理鼠标事件
           ProcessMouseEvent();

       if (eventSystem.sendNavigationEvents)
       {
           if (!usedEvent)
               usedEvent |= SendMoveEventToSelectedObject();

           if (!usedEvent)
               SendSubmitEventToSelectedObject();
       }
 }

 protected void ProcessMouseEvent()
 {
 	//鼠标左键事件Id为0
     ProcessMouseEvent(0);
 }

protected void ProcessMouseEvent(int id)
{
	//关键函数
    var mouseData = GetMousePointerEventData(id);
    var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

    m_CurrentFocusedGameObject = leftButtonData.buttonData.pointerCurrentRaycast.gameObject;

    // 处理鼠标左键点击
    ProcessMousePress(leftButtonData);
    ProcessMove(leftButtonData.buttonData);
    ProcessDrag(leftButtonData.buttonData);

    // 处理鼠标右键键点击
    ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
    ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
    ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
    ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);

    if (!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f))
    {
        var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
        ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
    }
}

GetMousePointerEventData方法是StandaloneInputModulePointerInputModule继承的

protected virtual MouseState GetMousePointerEventData(int id)
  {
       // Populate the left button...
       PointerEventData leftData;
       var created = GetPointerData(kMouseLeftId, out leftData, true);

       leftData.Reset();

       if (created)
           leftData.position = input.mousePosition;

       Vector2 pos = input.mousePosition;
       if (Cursor.lockState == CursorLockMode.Locked)
       {
           // We don't want to do ANY cursor-based interaction when the mouse is locked
           leftData.position = new Vector2(-1.0f, -1.0f);
           leftData.delta = Vector2.zero;
       }
       else
       {
           leftData.delta = pos - leftData.position;
           leftData.position = pos;
       }
       leftData.scrollDelta = input.mouseScrollDelta;
       leftData.button = PointerEventData.InputButton.Left;
       
       //发射射线
       eventSystem.RaycastAll(leftData, m_RaycastResultCache);
       
	  //省略后半部分代码----下面会分析
}
        

上面代码主要作用是发射射线,填充MouseButtonEventData 事件

EventSystemRaycastAll方法

 public class EventSystem : UIBehaviour
{
     public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults)
      {
          raycastResults.Clear();
          var modules = RaycasterManager.GetRaycasters();
          var modulesCount = modules.Count;
          for (int i = 0; i < modulesCount; ++i)
          {
              var module = modules[i];
              if (module == null || !module.IsActive())
                  continue;

			 //发射射线
              module.Raycast(eventData, raycastResults);
          }

          raycastResults.Sort(s_RaycastComparer);
      }
}

这个RaycasterManager会收集场景里面所有实现BaseRaycaster接口的类
在这里插入图片描述

由于我们场景里面只有Canvas上面挂载了GraphicRaycaster 组件
在这里插入图片描述

GraphicRaycaster.Raycast详细解析可以查看GraphicRaycaster .Raycast详解,下面给出部分源码

 public class GraphicRaycaster : BaseRaycaster
 {
    private List<Graphic> m_RaycastResults = new List<Graphic>();
	public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)   {
		//。。。省略部分代码。。。
		//拿到所有继承了Graphic的类,Button继承了该类
	 	Raycast(canvas, currentEventCamera, eventPosition, canvasGraphics, m_RaycastResults);
	 	//。。。省略部分代码。。。
 		int totalCount = m_RaycastResults.Count;
        for (var index = 0; index < totalCount; index++){
		//。。。省略部分代码。。。
		//m_RaycastResults进行结果排序
		//构建结果返回
		var castResult = new RaycastResult
        {
        	//这个就是前面ReleaseMouse方法里的currentOverGo,也就是测试场景里的Text组件
              gameObject = go,
              module = this,
              distance = distance,
              screenPosition = eventPosition,
              displayIndex = displayIndex,
              index = resultAppendList.Count,
              depth = m_RaycastResults[index].depth,
              sortingLayer = canvas.sortingLayerID,
              sortingOrder = canvas.sortingOrder,
              worldPosition = ray.origin + ray.direction * distance,
              worldNormal = -transForward
          };
	       resultAppendList.Add(castResult);
	  }
}   
 

可以看到拿到的结果有两个
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

继续查看PointerInputModule.GetMousePointerEventData方法

protected virtual MouseState GetMousePointerEventData(int id)
{

	   //省略前面的代码,之前分析过了
		
	   //发射射线
	   eventSystem.RaycastAll(leftData, m_RaycastResultCache);
		//拿到第一个结果,即Text所在对象
       var raycast = FindFirstRaycast(m_RaycastResultCache);
       
       //赋值给leftData.pointerCurrentRaycast,后面需要用到
       leftData.pointerCurrentRaycast = raycast;
       m_RaycastResultCache.Clear();

       // copy the apropriate data into right and middle slots
       PointerEventData rightData;
       GetPointerData(kMouseRightId, out rightData, true);
       rightData.Reset();

       CopyFromTo(leftData, rightData);
       rightData.button = PointerEventData.InputButton.Right;

       PointerEventData middleData;
       GetPointerData(kMouseMiddleId, out middleData, true);
       middleData.Reset();

       CopyFromTo(leftData, middleData);
       middleData.button = PointerEventData.InputButton.Middle;

       m_MouseState.SetButtonState(PointerEventData.InputButton.Left, StateForMouseButton(0), leftData);
       m_MouseState.SetButtonState(PointerEventData.InputButton.Right, StateForMouseButton(1), rightData);
       m_MouseState.SetButtonState(PointerEventData.InputButton.Middle, StateForMouseButton(2), middleData);

       return m_MouseState;
   }

关键代码

var raycast = FindFirstRaycast(m_RaycastResultCache);

会拿到第一个点击到的物体,所以返回的是Text组件所在GameObject


处理点击事件函数ProcessMousePress

/// <summary>
/// 计算并处理任何鼠标按钮状态的变化。
/// </summary>
protected void ProcessMousePress(MouseButtonEventData data)
{
    // 获取当前的指针事件数据
    var pointerEvent = data.buttonData;
    
    // 这个对象就是前面拿到的Text所在的对象
    var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

    // 处理按下事件
    if (data.PressedThisFrame())
    {
        // 标记该事件为可点击
        pointerEvent.eligibleForClick = true; //关键代码,前面释放鼠标需要用到
        
        // 重置增量位置
        pointerEvent.delta = Vector2.zero;
        
        // 重置拖动标志
        pointerEvent.dragging = false;
        
        // 使用拖拽阈值
        pointerEvent.useDragThreshold = true;
        
        // 设置按下位置
        pointerEvent.pressPosition = pointerEvent.position;
        
        // 设置按下时的射线检测结果
        pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

        // 如果选择的对象发生了变化,则取消之前的选中状态
        DeselectIfSelectionChanged(currentOverGo, pointerEvent);

        // 查找继承了IPointerDownHandler的控件,因为Text没有实现改接口,所以向上查找可以处理点击事件的对象,找到了Button对象
        var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
        var newClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

        // 如果没有找到按下处理器,则查找点击处理器
        if (newPressed == null)
            newPressed = newClick;

        // Debug.Log("Pressed: " + newPressed);

        float time = Time.unscaledTime;

        // 如果新的按下对象与上次相同,则增加点击计数
        if (newPressed == pointerEvent.lastPress)
        {
            var diffTime = time - pointerEvent.clickTime;
            if (diffTime < 0.3f)
                ++pointerEvent.clickCount;
            else
                pointerEvent.clickCount = 1;

            pointerEvent.clickTime = time;
        }
        else
        {
            pointerEvent.clickCount = 1;
        }

        // 设置按下对象、原始按下对象和点击对象
        pointerEvent.pointerPress = newPressed;
        pointerEvent.rawPointerPress = currentOverGo;
        pointerEvent.pointerClick = newClick;

        pointerEvent.clickTime = time;

        // 保存拖拽处理器
        pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

        // 如果有拖拽处理器,执行初始化潜在拖拽操作
        if (pointerEvent.pointerDrag != null)
            ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);

        // 保存输入指针事件
        m_InputPointerEvent = pointerEvent;
    }

    // 处理释放事件
    if (data.ReleasedThisFrame())
    {
        ReleaseMouse(pointerEvent, currentOverGo);
    }
}

这里面 pointerEvent.eligibleForClick = true; 前面释放鼠标需要用到。

ExecuteEvents.ExecuteHierarchy最终执行按下事件

 public static class ExecuteEvents{
 
	private static void GetEventChain(GameObject root, IList<Transform> eventChain)
	  {
	       eventChain.Clear();
	       if (root == null)
	           return;
	
	       var t = root.transform;
	       while (t != null)
	       {
	           eventChain.Add(t);
	           t = t.parent;
	       }
	   }
	   
	 public static GameObject ExecuteHierarchy<T>(GameObject root, BaseEventData eventData, EventFunction<T> callbackFunction) where T : IEventSystemHandler
	  {
	  	   //获取事件链
	       GetEventChain(root, s_InternalTransformList);
	
	       var internalTransformListCount = s_InternalTransformList.Count;
	       for (var i = 0; i < internalTransformListCount; i++)
	       {
	           var transform = s_InternalTransformList[i];
	           //关键函数,执行点击事件
	           if (Execute(transform.gameObject, eventData, callbackFunction))
	               return transform.gameObject;
	       }
	       return null;
	   }
	    
	
	 public static bool Execute<T>(GameObject target, BaseEventData eventData, EventFunction<T> functor) where T : IEventSystemHandler
	 {
	     var internalHandlers = ListPool<IEventSystemHandler>.Get();
	     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
	         {
	         	 //最终执行事件
	             functor(arg, eventData);
	         }
	         catch (Exception e)
	         {
	             Debug.LogException(e);
	         }
	     }
	
	     var handlerCount = internalHandlers.Count;
	     ListPool<IEventSystemHandler>.Release(internalHandlers);
	     return handlerCount > 0;
	 }
	

	private static void GetEventList<T>(GameObject go, IList<IEventSystemHandler> results) where T : IEventSystemHandler
    {
          // Debug.LogWarning("GetEventList<" + typeof(T).Name + ">");
          if (results == null)
              throw new ArgumentException("Results array is null", "results");

          if (go == null || !go.activeInHierarchy)
              return;

          var components = ListPool<Component>.Get();
          go.GetComponents(components);

          var componentsCount = components.Count;
          for (var i = 0; i < componentsCount; i++)
          {
              if (!ShouldSendToComponent<T>(components[i]))
                  continue;

              // Debug.Log(string.Format("{2} found! On {0}.{1}", go, s_GetComponentsScratch[i].GetType(), typeof(T)));
              results.Add(components[i] as IEventSystemHandler);
          }
          ListPool<Component>.Release(components);
          // Debug.LogWarning("end GetEventList<" + typeof(T).Name + ">");
      }
}

5.得到结果

GetEventChain拿到4个结果
在这里插入图片描述
在这里插入图片描述

  • 因为Text没有实现IPointerDownHandler,所以向父节点查找实现了该接口的组件
  • 也就是Button,最终触发IPointerDownHandler事件

在这里插入图片描述
在这里插入图片描述

总结如下

EventSystem会在每帧检测鼠标左键是否按下,如果按下,发射射线,拿到第一个检测到的物体(通过排序机制得到),执行IPointerDownHandler事件,同时构造好PointerEventData数据给后面释放鼠标时使用,在鼠标释放时,查找点击的GameObject上是否有组件实现了IPointerClickHandler接口,如果有就触发事件,如果没有,就向父节点GameObject查找,直到找到发出射线的Canvas为止。

三、总结

阶段内容
📌 Unity 引擎调用EventSystem.Update()
🔁 每帧更新TickModules()StandaloneInputModule.UpdateModule()
🖱️ 鼠标释放ReleaseMouse() 被调用
🎯 查找点击对象使用 GetEventHandler<IPointerClickHandler>()
⚡ 执行点击ExecuteEvents.Execute()handler.OnPointerClick()
🧱 Button 响应OnPointerClick()Press()m_OnClick.Invoke()
📈 用户监听onClick.AddListener(() => { ... }) 中的方法被调用

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值