一、引子

  马三在最近的开发工作中遇到了一个比较有意思的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 = ;
  41.  
  42. pointerEvent.clickTime = time;
  43. }
  44. else
  45. {
  46. pointerEvent.clickCount = ;
  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吗?的更多相关文章

  1. 【Unity3d游戏开发】浅谈UGUI中的Canvas以及三种画布渲染模式

    一.Canvas简介 Canvas画布是承载所有UI元素的区域.Canvas实际上是一个游戏对象上绑定了Canvas组件.所有的UI元素都必须是Canvas的自对象.如果场景中没有画布,那么我们创建任 ...

  2. 【Unity游戏开发】浅谈 NGUI 中的 UIRoot、UIPanel、UICamera 组件

    简介 马三最近换到了一家新的公司撸码,新的公司 UI 部分采用的是 NGUI 插件,而之前的公司用的一直是 Unity 自带的 UGUI,因此马三利用业余时间学习了一下 NGUI 插件的使用,并把知识 ...

  3. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二)

    本帖是延续的:C# Unity游戏开发——Excel中的数据是如何到游戏中的 (一) 上个帖子主要是讲了如何读取Excel,本帖主要是讲述读取的Excel数据是如何序列化成二进制的,考虑到现在在手游中 ...

  4. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (三)

    本帖是延续的:C# Unity游戏开发——Excel中的数据是如何到游戏中的 (二) 前几天有点事情所以没有继续更新,今天我们接着说.上个帖子中我们看到已经把Excel数据生成了.bin的文件,不过其 ...

  5. C# Unity游戏开发——Excel中的数据是如何到游戏中的 (四)2018.4.3更新

    本帖是延续的:C# Unity游戏开发--Excel中的数据是如何到游戏中的 (三) 最近项目不算太忙,终于有时间更新博客了.关于数据处理这个主题前面的(一)(二)(三)基本上算是一个完整的静态数据处 ...

  6. 【Unity游戏开发】浅谈Lua和C#中的闭包

    一.前言 目前在Unity游戏开发中,比较流行的两种语言就是Lua和C#.通常的做法是:C#做些核心的功能和接口供Lua调用,Lua主要做些UI模块和一些业务逻辑.这样既能在保持一定的游戏运行效率的同 ...

  7. 【Unity游戏开发】用C#和Lua实现Unity中的事件分发机制EventDispatcher

    一.简介 最近马三换了一家大公司工作,公司制度规范了一些,因此平时的业余时间多了不少.但是人却懒了下来,最近这一个月都没怎么研究新技术,博客写得也是拖拖拉拉,周六周天就躺尸在家看帖子.看小说,要么就是 ...

  8. 喵的Unity游戏开发之路 - 推球:游戏中的物理

    很多童鞋没有系统的Unity3D游戏开发基础,也不知道从何开始学.为此我们精选了一套国外优秀的Unity3D游戏开发教程,翻译整理后放送给大家,教您从零开始一步一步掌握Unity3D游戏开发. 本文不 ...

  9. 关于Unity游戏开发方向找工作方面的一些个人看法

     这是个老生常谈,却又是谁绕不过去的话题,而对于每个人来说,所遇到的情况又不尽相同,别人的求职方式和路线不一定适合你,即使是背景很相似的两个人,有时候机遇也很重要. 我本人的工作经验只有一年,就业方式 ...

随机推荐

  1. 在项目管理中如何保持专注,分享一个轻量的时间管理工具【Flow Mac版 - 追踪你在Mac上的时间消耗】

    在项目管理和团队作业中,经常面临的问题就是时间管理和优先级管理发生问题,项目被delay,团队工作延后,无法达到预期目标. 这个仿佛是每个人都会遇到的问题,特别是现在这么多的内容软件来分散我们的注意力 ...

  2. SQLServer之锁简介

    锁定义(Definition) 锁定是 DBMS 将访问限制为多用户环境中的行的过程. 以独占方式锁定行或列,不允许其他用户访问锁定的数据,直到锁被释放. 这可确保两个用户不能同时更新行中的同一列. ...

  3. 记录一次Orthanc dicom数据异常手动修复

    问题复现场景 同一个StudyInstanceUID,对应两个不同的PatientID. 通俗讲,原本是一个病人的一次影像,却割裂成两个病人的影像,虽然两个病人不影响系统数据,但是同一个Study分别 ...

  4. Python之Pandas的一些理解

    Pandas的功能: 1.  结构化的数据分析; 相比excel,可以处理更大量的数据和更好的性能 2.  对数据的清洗

  5. WIn10系统软件默认安装c盘后消失看不见问题

    一.win10系统下c盘,program 文件下 软件一般为32 或者 64位,但是现在win10系统有些C盘会显示program  x86 向这种情况的话我们的软件默认安装在这个盘的话可能会造成很多 ...

  6. Linux新手随手笔记

    RPM通过将安装规则与源代码打包到一起,来降低软件的安装难度 yum 通过将大量的常用RPM软件存放在一起,解决软件包之间的依赖关系,进一步降低软件的安装难度 rhel 5\6 init rhel 7 ...

  7. Python函数的装饰器修复技术(@wraps)

    @wraps 函数的装饰器修复技术,可使被装饰的函数在增加了新功能的前提下,不改变原函数名称,还继续使用原函数的注释内容: 方便了上下文环境中不去更改原来使用的函数地方的函数名: 使用方法 from ...

  8. 关于ES6的module的循环加载

    今天写js时,碰到了一个模块循环加载的错误,下面时例子: // testa.mjs import testb from './testb.mjs'; const {b} = testb; const ...

  9. JS 设计模式七 -- 模板方法模式

    概念 模板方法模式是一直昂只需使用继承就可以实现的非常简单的模式. 模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体实现的子类. 实现 模板方法模式一般的实现方式为继承. // 体育运 ...

  10. [Alpha阶段]测试报告

    [Alpha]阶段测试报告 在测试过程中发现的BUG ​ 在最后的测试阶段中,我们不可避免的遇到了各种各样的BUG.虽然大多数都不是严重的BUG,但是这些细枝末节的问题的堆积,依然会很大程度上降低用户 ...