寫完《Unity4.6新UI系統初探》後,我模仿手機上的UI分别用uGui和NGUI做了一個僅用作示範的ToggleSlider,我認為這個小小的控件已能展現自定義控件的開發過程。由于手頭上沒有mac版,暫時未能真機測試,PC上的效果如下:

完整工程托管于github,分為uGui和NGUI兩個project。考慮到版權問題,工程裡不含NGUI,同學們需自行将NGUI導進工程。NGUI需要Unity 4.5,uGui需要Unity 4.6。
滑塊可以拖動,從一邊拖到另一邊将改變控件值。
使用者停止操作時,滑塊如果居中,會自動滑向最近的一邊。
點選滑塊或整個控件,控件值将被改變,滑塊自動滑向另一邊。
控件值被其它腳本修改時,滑塊自動滑向另一邊。
滑塊移動的過程中,如果值發生變化,滑塊會以目前位置為起點滑回去。
下面以uGui為例簡述制作方法,NGUI的方法也差不多,兩者的差別可參考下文[和NGUI對比]。
上圖是用uGui制作好的層級結構。其中,
Canvas負責渲染UI。
Padding沒什麼用,隻是畫了一個邊框。
Toggle Slider是控件的父物體。
BackgroundAndMask使用ImageMask元件作為SymbolOff的遮罩,同時渲染灰色底圖。
SymbolOff是灰色的twitter小鳥,坐标受動畫控制。
Background_On使用Image元件渲染藍色高亮底圖,Color.alpha受動畫控制。
BackgroundMaskOnly使用ImageMask元件作為SymbolOn的遮罩,并不渲染。
SymbolOn是藍色的twitter小鳥,坐标受動畫控制。(不用Background_On作遮罩是因為藍色底圖的邊緣是半透明的。)
Handle是正方形滑塊,坐标隻受動畫控制。
Current Value是下面那個可選框,用于測試Toggle Slider。
EventSystem可參考上篇文章。
Toggle Slider對象包含的Toggle Slider元件是唯一一個直接和控件有關的腳本。代碼可在github查閱,編寫起來很簡單。
所有效果都使用Animation元件實作,全部用動畫是為了偷懶,畢竟效果怎麼實作都可以,這裡僅作示範。動畫包含四條曲線,分别用于控制兩隻twitter小鳥、藍色背景透明度和滑塊左右移動。這裡簡單提幾個要點。
動畫的反向播放隻需要将AnimationState.speed設為-1。
拖拽滑塊時,動畫暫停,根據滑鼠位移逐幀設定動畫時間,然後Sample動畫。拖拽停止時恢複動畫。
在動畫裡改變透明度時,Image元件不會自動更新,需要添加一個ColorWatcher元件,自己觸發Image.color的setter。
動畫設為ClampForever,因為Once無法在AnimationState中保留最後一幀的狀态。
事件使用兩個Event Trigger元件進行響應。一個在Toggle Slider對象裡,負責響應OnPointerUp,實作當點選控件時,調用ToggleSlider.Toggle()。另一個在Handle對象裡 (如圖),負責響應Drag事件,實作當拖動時調用ToggleSlider.OnDrag()。
在此遇到了一個蛋疼的問題,Event Trigger的Drop事件在這裡無效,又沒有單獨的DragEnd事件,是以隻好在Handle上增加OnPointerUp事件來監聽拖動是否結束。如此一來,Handle的OnPointerUp就會把上層控件的OnPointerUp事件攔截掉……我希望Unity能提供類似冒泡的機制,這樣一來我就能在Handle上添加一個腳本,隻對拖拽結束進行響應,如果是單擊事件就冒泡到上層控件進行處理。
最終我的做法是,Handle的OnPointerUp事件也由ToggleSlider.OnPointerUp()響應,OnPointerUp内部通過dragging标記來判斷是拖動結束還是單擊。
Event Trigger沒有冒泡機制,子控件如果不處理事件,沒辦法抛給父控件處理。
ImageMask沒能選擇alpha threshold。
這段時間的測試遇到過幾個問題:
經常警告"Material uGUI/Stencil Mask doesn't have stencil ... SendWillRenderCanvases()"。有時會導緻Image無法顯示,要換過一次Sprite之後才正常。
兩個Hierarchy内平級并且相鄰的ImageMask,都選中DrawImage,結果上面一個會擋住下面一個。需要在兩個中間插入一包含CanvasRenderer的GO才行,GO可以deactivated。
當我制作NGUI版本時,從uGui複制了一份出來再做修改。做到一半時我發現Hierarchy多了一個不含子物體的副本,當我選中控件時副本會同時被選中。于是我重新開機Unity,發現Unity已經死鎖無法關閉,強制結束後項目損壞,隻要一打開就crash,手動删除scene後才恢複正常。估計是我在繼承樹上混用NGUI/uGui,或者uGui未剔除幹淨引起的,已向官方回報。
作為對比,我也用NGUI的測試版(3.6.4b)做了一樣的demo,花了不少時間。uGui的事件問題也在NGUI裡遇到了,甚至更嚴重,此外還有其它問題。
NGUI的padding設定挺繁瑣的,uGui隻要Rect Transform點下stretch,Left/Top/Right/Bottom全寫20就行。
添加padding時,我試着建立一個UIWidget,然後設定Anchors為Unified,然後依次設定Left/Top/Right/Bottom為Target's Left/Top/Right/Bottom,然後數值填入20/-20/20/-20才行。
NGUI添加Toggle有點複雜,uGui隻要Hierarchy裡Create一個就完事了。
建立調試用的Current Value時,找不到NGUI的Toggle元件,後來輸入名稱才找到,但還是不太會用。後來想到Examples裡有toggle的prefab可以用,拖進Scene後對比了下發現NGUI的實作方式比uGui複雜了些,難以手工建立出來。看來Project裡要把NGUI這些常見庫都備好才行。
NGUI設定Anchor有點失敗
将Toggle的prefab執行個體化到scene裡後,設定了很久都沒能讓Toggle自動居中。難道這個Toggle的尺寸如果是動态的,NGUI就沒辦法自動居中?或許是我對NGUI還不是很了解,最終我隻能根據Toggle寬度算出坐标偏移。
NGUI沒有Image Mask
是以這個版本沒能加入那兩個twitter的logo。這個怪不了NGUI,因為Unity的free版不提供通路stencil buffer的功能,是以第三方UI插件沒辦法實作比較好的clipping機制。
NGUI的UIEventTrigger無法獲得事件參數
UIEventTrigger和uGui的EventTrigger類似,能夠觸發遠端方法。但是NGUI不能傳入動态事件參數,雖然能用 UIEventTrigger.current獲得目前事件,可UIEventTrigger對象其實沒定義任何參數。要獲得參數,隻能自己寫一個帶有 OnDrag的元件,附加到GameObject上,或者使用UIEventListener,總之就不支援可視化編輯,隻能用代碼動态綁定事件。
NGUI的UIEventListener無法響應停止拖動事件
為了解決前一個問題,我使用了UIEventListener來獲得拖動參數。然而當我想響應停止拖動事件時,我發現還是要用回UIEventTrigger才行。如果使用者不希望混用這兩個腳本,那麼隻能自己寫一個腳本。
uGui功能和使用者體驗方面都做的不錯,是我看到過最貼近Unity風格的UI系統。穩定性方面有小問題,不過作為測試版可以了解,已經超過了我的預期(之前以為會和4.0的剛推出Mecanim一樣bug一堆)。
性能方面,兩個工程我都實作了相同的PackedBenchMark場景,裡面各包含了30個ToggleSlider,為了公平uGUI版本去掉了所有ImageMask,兩邊實測drawcall一緻。從幀率上看在編輯器下NGUI性能優于uGUI大約20%!估計是因為NGUI在最近幾個版本中完善了batching機制,而uGUI并沒有采用前一篇文章所說的"更優的"batch算法,而是把batching粗暴的交給了顯示卡驅動完成。如果有pro版的話使用profiler檢視一下兩邊的CPU/GPU占用就能知道答案。
本文作者:Jare @ 夢加網絡
本文轉載自https://github.com/jaredoc/unity-ugui/tree/master/toggle_demo