1. 程式人生 > >【Unity遊戲開發】你真的了解UGUI中的IPointerClickHandler嗎?

【Unity遊戲開發】你真的了解UGUI中的IPointerClickHandler嗎?

ali rev public 觸摸事件 proc potential 接下來 unity3 哪裏

一、引子

  馬三在最近的開發工作中遇到了一個比較有意思的bug:“TableViewCell上面的某些自定義UI組件不能響應點擊事件,並且它的父容器TableView也不能響應點擊事件,但是TableViewCell上面的Button等組件卻可以接受點擊事件,並且如果單獨把自定義UI控件放在一個UI上面也可以接受點擊事件”。最後馬三通過仔細地分析,發現是某些自定義的UI組件實現方法的問題。通常情況下,如果想要一個UI響應點擊事件的話,我們只需要實現IPointerClickHandler這個接口就可以了,但是在我們項目中的TableView繼承自MonoBehavior,並且實現了IPointerClickHandler, IPointerDownHandler, IPointerUpHandler,IDragHandler等UI接口,此時如果我們的自定義UI組件只實現了IPointerClickHandler接口,而沒有實現 IPointerDownHandler 接口,然後又作為TableViewCell裏面的一個Child的話,就會出現TableViewCell接收不到點擊事件,TableView也接收不到點擊事件。點擊事件被詭異地“吞沒了”!下面我們簡單地設計三個不同情況下的模擬測試來復現一下這個bug。

二、進行測試

情況1:沒有父節點,自己身上掛載的腳本只實現IPointerClickHandler接口:

  場景中只有一個類型為Image的普通節點,它身上掛載了一個名為ChildHandler的腳本,該腳本只實現IPointerClickHandler接口

技術分享圖片

 1 using System.Collections;
 2 using System.Collections.Generic;
 3 using UnityEngine;
 4 using UnityEngine.EventSystems;
 5 
 6 public class ChildHandler : MonoBehaviour, IPointerClickHandler
7 { 8 public void OnPointerClick(PointerEventData eventData) 9 { 10 Debug.Log("Child OnPointerClick" + eventData.ToString()); 11 } 12 }

  運行遊戲,點擊Image組件,觀察控制臺輸出結果如下,這種情況下,我們只實現了IPointerClickHandler接口便接收到了點擊事件。

技術分享圖片

情況2:有父節點,父節點掛載的腳本實現了IPointerClickHandler, IPointerDownHandler, IPointerUpHandler接口,自己身上掛載的腳本亦實現同樣的接口:

  然後我們再建立一個名為Parent的父節點,將Child子節點移動到Parent節點的內部。Parent節點掛載ParentHandler腳本,該腳本實現IPointerClickHandler, IPointerDownHandler, IPointerUpHandler接口。Child子節點掛載ChildHandler腳本,該腳本跟ParentHandler腳本實現相同的接口。

技術分享圖片

 1 using System.Collections;
 2 using System.Collections.Generic;
 3 using UnityEngine;
 4 using UnityEngine.EventSystems;
 5 
 6 public class ParentHandler : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
 7 {
 8     public void OnPointerClick(PointerEventData eventData)
 9     {
10         Debug.Log("Parent OnPointerClick" + eventData.ToString());
11     }
12 
13     public void OnPointerDown(PointerEventData eventData)
14     {
15         Debug.Log("Parent OnPointerDown" + eventData.ToString());
16     }
17 
18     public void OnPointerUp(PointerEventData eventData)
19     {
20         Debug.Log("Parent OnPointerUp" + eventData.ToString());
21     }
22 }
 1 using System.Collections;
 2 using System.Collections.Generic;
 3 using UnityEngine;
 4 using UnityEngine.EventSystems;
 5 
 6 public class ChildHandler : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
 7 {
 8     public void OnPointerClick(PointerEventData eventData)
 9     {
10         Debug.Log("Child OnPointerClick" + eventData.ToString());
11     }
12 
13     public void OnPointerDown(PointerEventData eventData)
14     {
15         Debug.Log("Child OnPointerDown" + eventData.ToString());
16     }
17 
18     public void OnPointerUp(PointerEventData eventData)
19     {
20         Debug.Log("Child OnPointerUp" + eventData.ToString());
21     }
22 }

  運行遊戲,分別點擊Child區域和Parent區域,觀察控制臺輸出結果,可以發現子節點和父節點都可以分別接收到到點擊事件。

技術分享圖片

情況3:有父節點,父節點掛載的腳本實現了IPointerClickHandler, IPointerDownHandler, IPointerUpHandler接口,自己身上掛載的腳本只實現IPointerClickHandler接口:

  接著我們再來看最後一種情況,它跟上面的情況差不多,不同的是ChildHandler只實現了IPointerClickHandler接口,而沒有實現 IPointerDownHandler, IPointerUpHandler另外兩個接口:

 1 using System.Collections;
 2 using System.Collections.Generic;
 3 using UnityEngine;
 4 using UnityEngine.EventSystems;
 5 
 6 public class ChildHandler : MonoBehaviour, IPointerClickHandler
 7 {
 8     public void OnPointerClick(PointerEventData eventData)
 9     {
10         Debug.Log("Child OnPointerClick" + eventData.ToString());
11     }
12 }

  運行遊戲,分別點擊Child區域和Parent區域,觀察控制臺輸出結果,可以發現無論我們如何點擊Child區域都無法接收到Click事件,並且這個Click事件也沒有傳遞到父節點中。正如我們開篇所說的一樣,父節點只接收到了Down和Up的事件,Click事件被“吞沒了”。點擊子節點沒有和父節點重疊的地方,父節點正常地接收到了點擊事件和Down、Up的事件。

技術分享圖片

  那麽我們的Click事件去哪裏了呢?到底是被誰給偷偷吃掉了呢?我們不妨從分析UGUI的源碼入手,分析一下問題所在,再次貼上UGUI的源碼傳送門。

三、分析原因與源碼

  因為我們是在Windows平臺進行測試的,所以我們打開StandaloneInputModule.cs這個腳本進行觀察,我們直接來到第431行ProcessMouseEvent函數,這個函數負責處理鼠標的事件。

技術分享圖片

  裏面就一行調用,調用了ProcessMouseEvent這個函數,那麽我們再繼續觀察ProcessMouseEvent的內容:
技術分享圖片

  重點關註一下453行的ProcessMousePress方法,它處理了鼠標的左鍵點擊,那麽我們就以鼠標左鍵點擊來繼續往下分析一下,完整的ProcessMousePress函數代碼如下:

  1         /// <summary>
  2         /// Process the current mouse press.
  3         /// </summary>
  4         protected void ProcessMousePress(MouseButtonEventData data)
  5         {
  6             var pointerEvent = data.buttonData;
  7             var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;
  8 
  9             // PointerDown notification
 10             if (data.PressedThisFrame())
 11             {
 12                 pointerEvent.eligibleForClick = true;
 13                 pointerEvent.delta = Vector2.zero;
 14                 pointerEvent.dragging = false;
 15                 pointerEvent.useDragThreshold = true;
 16                 pointerEvent.pressPosition = pointerEvent.position;
 17                 pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;
 18 
 19                 DeselectIfSelectionChanged(currentOverGo, pointerEvent);
 20 
 21                 // search for the control that will receive the press
 22                 // if we can‘t find a press handler set the press
 23                 // handler to be what would receive a click.
 24                 var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.pointerDownHandler);
 25 
 26                 // didnt find a press handler... search for a click handler
 27                 if (newPressed == null)
 28                     newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
 29 
 30                 // Debug.Log("Pressed: " + newPressed);
 31 
 32                 float time = Time.unscaledTime;
 33 
 34                 if (newPressed == pointerEvent.lastPress)
 35                 {
 36                     var diffTime = time - pointerEvent.clickTime;
 37                     if (diffTime < 0.3f)
 38                         ++pointerEvent.clickCount;
 39                     else
 40                         pointerEvent.clickCount = 1;
 41 
 42                     pointerEvent.clickTime = time;
 43                 }
 44                 else
 45                 {
 46                     pointerEvent.clickCount = 1;
 47                 }
 48 
 49                 pointerEvent.pointerPress = newPressed;
 50                 pointerEvent.rawPointerPress = currentOverGo;
 51 
 52                 pointerEvent.clickTime = time;
 53 
 54                 // Save the drag handler as well
 55                 pointerEvent.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);
 56 
 57                 if (pointerEvent.pointerDrag != null)
 58                     ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.initializePotentialDrag);
 59             }
 60 
 61             // PointerUp notification
 62             if (data.ReleasedThisFrame())
 63             {
 64                 // Debug.Log("Executing pressup on: " + pointer.pointerPress);
 65                 ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);
 66 
 67                 // Debug.Log("KeyCode: " + pointer.eventData.keyCode);
 68 
 69                 // see if we mouse up on the same element that we clicked on...
 70                 var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
 71 
 72                 // PointerClick and Drop events
 73                 if (pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick)
 74                 {
 75                     ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
 76                 }
 77                 else if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
 78                 {
 79                     ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
 80                 }
 81 
 82                 pointerEvent.eligibleForClick = false;
 83                 pointerEvent.pointerPress = null;
 84                 pointerEvent.rawPointerPress = null;
 85 
 86                 if (pointerEvent.pointerDrag != null && pointerEvent.dragging)
 87                     ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);
 88 
 89                 pointerEvent.dragging = false;
 90                 pointerEvent.pointerDrag = null;
 91 
 92                 // redo pointer enter / exit to refresh state
 93                 // so that if we moused over somethign that ignored it before
 94                 // due to having pressed on something else
 95                 // it now gets it.
 96                 if (currentOverGo != pointerEvent.pointerEnter)
 97                 {
 98                     HandlePointerExitAndEnter(pointerEvent, null);
 99                     HandlePointerExitAndEnter(pointerEvent, currentOverGo);
100                 }
101             }
102         }

  在這個函數中首先會拿到射線檢測返回的gameobject,然後搜索當前的gameobejct以及其父節點上面是否有實現了IPointerDownHandler的接口的控件,如果有實現了的就把newPressed賦值為這個控件的gameobject,如果沒有,就去搜索實現了IPointerClickHandler這個接口的控件,如果沒有在自身上找到的話,會依次地向父節點層層搜索,直到找到為止,然後依然是把newPressed賦值為這個控件的gameobject。接著會按照類似的方式去搜索自身以及父節點上是否有實現了IDragHandler的組件,如果有的話緊接著便會去觸發OnPointerDown和OnDrag方法。

  當鼠標按下並擡起的時候,首先會觸發IPointerUpHandler接口中的函數OnPointerUp(),然後會再次搜索當前gameobject以及其父節點上是否有實現了IPointerClickHandler接口的控件,如果有的的話,會和之前存下來的newPressd進行比較,看兩者是否為同一個gameobject。如果兩者為同一個gameobject的話就會觸發Click事件。那麽問題就出現在這裏了,Unity原本想用這段代碼判斷鼠標按下和擡起的時候,鼠標指向的物體有沒有變化。如果有變化,前後指向的不是同一個gameobject的話就不觸發Click事件了。雖然原本是想實現這個功能,但是當我們的父節點實現了IPointerDownHandler和IPointerClickHandler接口,而子節點只實現了IPointerClickHandler接口的時候,就會造成兩次獲取的gameobject不匹配,那麽也就不會觸發任何的Click事件了,所以無論是父節點亦或者子節點腳本中的OnPointerClick方法也不會被調用到了,看來Click事件就是被這裏“吃掉了”。雖然在這裏我們只分析了Windows平臺下的鼠標點擊實現,但是在Mobile平臺上,在觸摸事件的處理上也是使用了類似的手段,也就是說這個bug也會在Android或者iOS平臺上出現。

  因此我們需要註意,如果一個物體沒有父節點的話,那麽只實現IPointerClickHandler接口便是可以接收到點擊事件的。如果他有父節點,父節點掛載的腳本也是只實現IPointerClickHandler接口的話,點擊事件也是可以接收到的。但是如果父節點實現了IPointerDownHandler和IPointerClickHandler接口,子節點只實現IPointerClickHandler接口的話,兩者便會都接收不到點擊事件,需要子節點也實現IPointerDownHandler這個接口才行。

三、總結

  通過一系列的試驗和對UGUI源碼地分析,我們弄明白了Click事件為什麽消失不見了,以及UGUI接口使用中的一些需要註意的小細節和坑。看來只顧悶頭寫業務邏輯是完全不夠的啊,在必要的時候,我們需要“沈下去”,去閱讀更底層的源碼,去分析bug出現的根本原因,這樣才能起到“標本兼治”的效果,這樣我們寫起代碼來才能更加安心。同時通過閱讀源碼,對源碼進行分析和思考,也可以提升我們的編碼水平、深化編程思想。因此馬三決定會在接下來的博客計劃中開辟出一個系統分析UGUI源碼的系列文章,讓我們一起來“扒開UGUI的祖墳”。

  本篇博客中的項目代碼已經同步至Github,歡迎Fork!https://github.com/XINCGer/Unity3DTraining/tree/master/SomeTest/About_IPointerClickHandler

如果覺得本篇博客對您有幫助,可以掃碼小小地鼓勵下馬三,馬三會寫出更多的好文章,支持微信和支付寶喲!

 技術分享圖片 技術分享圖片

作者:馬三小夥兒
出處:https://www.cnblogs.com/msxh/p/10588783.html
請尊重別人的勞動成果,讓分享成為一種美德,歡迎轉載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評指正。留下你的腳印,歡迎評論!

【Unity遊戲開發】你真的了解UGUI中的IPointerClickHandler嗎?