• 【Unity游戏开发】你真的了解UGUI中的IPointerClickHandler吗?


    一、引子

      马三在最近的开发工作中遇到了一个比较有意思的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 
    请尊重别人的劳动成果,让分享成为一种美德,欢迎转载。另外,文章在表述和代码方面如有不妥之处,欢迎批评指正。留下你的脚印,欢迎评论!

  • 相关阅读:
    数组、链表、Hash的优缺点
    数据库-索引的坏处,事务的级别,分布式事务的原理。
    4G内存的电脑,如何读取8G的日志文件进行分析,汇总数据成报表的面试题
    数据库常用的锁有哪些
    2020年最新 C# .net 面试题,月薪20K+中高级/架构师必看(十)
    ThreadX应用开发笔记之一:移植ThreadX到STM32平台
    net core 方法 返回值 重改?
    使用RestTemplate发送HTTP请求举例
    dedecms织梦手机站上一篇下一篇链接错误的解决方法
    多目标跟踪之数据关联(匈牙利匹配算法和KM算法)
  • 原文地址:https://www.cnblogs.com/msxh/p/10588783.html
Copyright © 2020-2023  润新知