公司要做一个这东西。
A是滑动区域,ScrollRect组件。
B是各种选项。
C是拾取到鼠标(或触点)的选项。
D是拖放区域。
大概要求是这样。
因为B的条目很多,放在A里可以滑动查看。如果要选择一个B,需要长按B,待时间足够之后生产一个新的C。拖动到D区域释放,则给D添加一个节点。其他区域则取消。
如果按住B的时间不够长,又动了鼠标(或触点),则当前滑动操作由A响应,产生A的滑动效果。
这里涉及到一个消息透传的问题。
解释一下自己在做的过程中采用的2个方案以及各自问题。
涉及的函数有以下几个:
OnPointerDown
OnPointerUp
OnDrag
将最终结果放这里,你要不愿看完,直接拿东西走人。
消息重置代码。
eventData.pointerEnter = m_Scro.gameObject; eventData.pointerPress = m_Scro.gameObject; eventData.rawPointerPress = m_Scro.gameObject; eventData.pointerDrag = m_Scro.gameObject; m_Scro.OnBeginDrag(eventData);
方案1:
win7+unity531。
刚开做的时候使用的531版本。在这个版本上做了些测试。得到以下数据:
- 使用EventTrigger,只有最上面的ui可以获得输入并调用EventTrigger设置的函数。
- 如果不使用EventTrigger,则需要自己继承一些类。需要什么类可以在EventTrigger里根据你需要的函数进行查询。比如你在EventTrigger中使用了Pointer Up消息。直接在unity文档的脚本分类下查询pointerup即可找到这个消息由哪个类分发,继承一下,自己重写一下这个函数就好。
- 如果不使用EventTrigger,采用继承消息类的方式工作,那么UI是否响应输入由Raycast Target这个选项决定。(使用EventTrigger的情况下没测,因为那个满足不了需求了。)
- 当你对一个Ui进行了操作之后,如果不做任何修改和特别处理,默认情况下,所有鼠标(或触点)操作均由这个UI控件接收。比如先在这个控件内OnPointerDown,然后按住鼠标不放,鼠标移动到别的Ui上,再放开,OnPointerUp函数依旧调用的是OnPointerDown时的UI。
- 如果只继承了OnPointerDown和OnPointerUp函数。则当OnPointerDown进入之后,如果产生了Drag输入,则会产生OnPointerUp事件。
- 再使用继承消息类及以上信息的情况下,A可以在即使点中B的情况下正常拖动。而且B的函数照进。
- 拖放操作响应函数是OnDrop,遵循第四条规则。但是如果我希望别的控件能够响应OnDrop,只需要把当前移动的这个UI的Raycast Target设置为false就好。(这里其实已经是一种消息透传了)
根据以上信息,于是我做了第一套方案:
大致如下:
- 当我按下B时,开启一个协程(等待1s,为长按时间)。如果协程没处理之前进入了OnPointerUp,则销毁协程。成功,则产生一个C,并将A设置enable为false,防止我在移动C的时候A跟着动。
- 生成的C交给一个全局管理器,这个管理器有一个m_Drag的GameObject对象。当该对象不为空,则在Update里帧更新这个对象的位置。用这个方法来实现拖动。
- 然后给D设置一个OnDrop函数。如果OnDrop函数调用,则通知全局管理器,C已被获取,清理。
- 全局管理器要判定一下,如果已经没有触点(使用Input.GetMouseButton(0)函数可以满足这个需求,虽然有意外情况,但至少正向流程OK),则销毁m_Drag
根据以上思路,大部分情况都解决了。只是有一个问题。全局管理器的触点判定是在Update里的。而OnDrop是触发的。逻辑是OnDrop响应之后做自身的处理,并通知全局管理器清空m_Drag一防止被Destroy。但并不知道当前这次的消息处理是在Update之前还是之后。如果在当前帧,鼠标触点已经释放,先进入Update,则在OnDrop被触发之前,m_Drag就会被Update先给Destroy掉。而如果OnDrop先调用,则可以确保正常。
在531下,测试结果显示,当前帧的输入处理在Update之前,于是方案一完成。
合并代码,对方用的535,于是我只好跟着升,这一升,坏了。拖动出问题了,拖出来放不进去了。
一检查,OnDrop跑到Update后面执行去了。
这样一来,没有合适的地方可以判断鼠标(或触点)的释放。于是只好考虑用OnPointerUp好了。可是OnPointerUp在控件被拖动的时候就会调用啊,根本没有机会或者说合适的地方可以产生OnPointerUp函数的调用,于是又做了一些测试如下:
- 如果只继承了OnPointerDown和OnPointerUp函数。则当OnPointerDown进入之后,如果产生了Drag输入,则会产生OnPointerUp事件。但如果再继承OnDrag,则会进入OnDrag事件而不是OnPointerUp。OnPointerUp将会在鼠标弹起(或触点被释放)时产生。
这是方案一第五条的补充。也是加入OnDrag之后逐步发现的,中间也调试了很久,出了很多麻烦事情。
有了这条,似乎一切都没问题了。于是加上,测试,一切OK。似乎完成了。然后我发现A在点到B的时候不再获取输入了。也就是说当我点到B的时候,A不能被滑动。Drag消息传给了B的脚本。
『
注:到这一步,我并没有尝试用关闭Raycast Target的方法来解决。原因有2:
- 我需要OnPointerUp,如果关闭,OnPointerUp想来是不会响应的,因为如果能通过关闭Raycast Target来将Drag传给A,那说明B已经不再获取输入了。
- 我需要彻底解决消息透传的问题,万一还有其他模块也会遇到类似的情况呢?
』
于是无法避免,考虑到以后也可能要用,只好来硬的,一定要解决消息透传的问题。
现在思路有2:
- 获取到我要控制的组件,在当前UI内,将输入信息传给这个要控制的组件,再修改。(这里就是在B内获取到Drag信息,将Drag信息传递给A对象,以控制A的滑动)
- 将输入消息重置。或新建一个,或将我希望获取输入的组件放进去。
翻了好久。EventSystem、BaseEventData、PointerEventData都翻过了,没有发现。
于是采用第一条思路。第一条思路也很容易。也许用在别的ui上已经没问题了。
但最让我伤心的是ScrollRect这个组件,如果你通过外部的方式(就是直接脚本改坐标)修改其容器的坐标,会弹回去。于是又去查怎么取消ScrollRect的回弹操作。一系列处理下来,似乎也能满足要求。然而因为第一次使用ugui,并没有发现有什么好的方案能让我在消息透传的时候让ScrollRect不弹回,而当我消息透传结束之后再开启弹回。就算有,界面上的小跳动,也无法直视。
于是又想。还是再看看,再嗖嗖,看看能不能重置消息。毕竟这才是正道。
然后解开了。怎么发现的我就不说了,纯属运气。我在查看EventSystem这个场景对象的时候,发现有个pointerDrag的对象。正好是我拖动的对象,我在想,如果改掉他会怎么样?
于是脚本里一搜pointerDrag,在PointerEventData类下。这就是消息函数传进来的参数啊。于是将pointerDrag直接改成了A,好的,一切工作正常,就是还会跳动。推测可能是因为中途突然加入输入信息导致,没有前提条件。于是将ScrollRect的Drag函数查询了一番,正好有个OnBeginDrag,加上,一切就正常了。
eventData.pointerEnter = m_Scro.gameObject; eventData.pointerPress = m_Scro.gameObject; eventData.rawPointerPress = m_Scro.gameObject; eventData.pointerDrag = m_Scro.gameObject; m_Scro.OnBeginDrag(eventData);
这就是消息重置。
就是当按住B的时间不够,产生了拖动,要将当前拖动消息交由A处理的核心代码。
其他消息相信也应该类似。