公司要做一個這東西。
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處理的核心代碼。
其他消息相信也應該類似。