說UI能延展出一丟丟的東西來,光java就有swing,swt/jface乃至javafx等等UI toolkit,在桌面上它們甚至都不是主流,在web端又有canvas、svg等等。
基於這些UI工具包\框架,又產生了大量通用的或者業務性的UI框架,比如Draw2d、GEF、easyUI乃至國內的EChart、白鷺等等。
這些框架的業務范圍各異,一個程序員的時間和精力有限,你不可能全部都掌握,又不能預言出是哪一個將來會獨步天下,甚至,連當前哪一個最流行,都夠打一陣嘴炮。
那,我們應該學什么?
本章節談談,如果,我們只有一個GC(graphic creator\capability)的時候,如何設計一套UI框架出來,了解了這些,在學習新的UI框架的時候,會更加容易。同時,這里提到的分析思路也可以應用到其他類型框架中,輔助學習。
切記,1、復雜由簡單構成;2、構成具備規律。
可想而知,再復雜的框架,划拉到底層的時候,也都是你早就應當掌握的基礎知識。不同的框架需要的基礎知識可能不一樣,但是規律(思想)大多是接近的。
我們先來定義一下,什么是GC?
每一個使用過上面提及的UI toolkit的程序員,應該都注意到了,它們很多都有提供“繪制”功能。在SWT中,提供該能力的是org.eclipse.swt.graphics.GC,在H5 canvas中,則是CanvasRender(也就是canvas element#getContext(‘2d’)得到的對象)。
它們有什么共同點?
可以設置ARGB信息,可以調用來繪制圖片、線段、形狀,等等。
為了方便起見,這里統一稱為GC,它是UI框架的基礎。
來思考一個問題,一個下圖所示的界面,如果用GC自行繪制的話,你要怎么做?
不用想太多,完整實現是不科學的,寫到頹,頹到禿。來看下最常見的解決方案吧:
把該界面上的各個部件拆開來,相同特性的歸為一類,每一類都提供一個繪制的方法,比如一個按鈕,就需要GC繪制四條邊、陰影線以及其中的文本。
然后,讓遍歷所有的部件,讓文本的歸文本,按鈕的歸按鈕。
有沒有突然感覺簡單了?這就是所謂的UI框架干的事兒。
如何構建這個UI框架呢?本文采取的思路是:面向對象建模。
具備面向對象知識的你,應當能分析出以下結論:控件,控件的布局,以及各種事件的處理,這三個元素組成了一個基本的UI框架。
1、控件
按鈕、文本框等可操作對象以及包裹它們的容器,這里稱之為控件。
所以,我們需要創建出以下對象,繼承關系以樹形結構表示:
其中父子級只表達繼承關系,Control和Composite它們的實體關系圖,則可能是如下所示:
其中,Control提供兩個方法:
paint(GC)負責調用GC,來繪制自身。
getBounds()負責提供當前控件的位置、大小信息,一般包括x,y,width,height。
Composite作為復合控件,它既具備Control的兩個方法,用於正確的繪制自身,又具備一個children列表,里面全都裝的是Control,當它的paint方法調用的時候,應當迭代自己的所有children,調用它們的paint。
具備此種結構之后,任何UI界面是不是都成為了某種單根結構?
根節點是一個Root Composite,葉子節點則是具體的某個Control。再聯想之前的打印窗口,它的實體結構大概會是這樣:
來活動活動思維吧,這是什么數據結構?各個控件的paint()方法調用順序是怎樣的?
2、布局
明明是有bounds的,為什么還需要“布局”呢?
其實啊,bounds(x,y,width,height)也可以視為一種布局,我稱之為自由布局,這種布局其實並不好,結論太粗暴難以接受么?大概講解一下:
你需要非常精確的控制x,y,width,height四個變量,設想你要制作一張兩行四列的表格,每個單元格你都得控制它們的位置,bounds信息如下所示:
[0,0,100,20],[100,0,100,20],[200,0,100,20],[300,0,100,20],
[0,20,100,20],[100,40,100,20],[200,60,100,20],[300,80,100,20]
如此你可以推論出一個公式,設i為行號,j為列號,單個cell的bounds信息公式為[i*width,j*height,width,height],注意到問題沒有,你需要自己維護一個嵌套循環,來為每一個單元格賦值。
控制相對位置需要花費大力氣。明確一個事實,在該“自由布局”里,child的范圍是有可能超出parent的邊框的(因為bounds的x,y目前指代的是GC使用到的x,y,也就是整個畫布的基准點),除非,你把每一個child的計算公式都改為[i*width+offsetX,j*height+offsetY,width,height],這里的offset代表parent的絕對位置。
很難控制縮放。比如你要對上述的表格進行縮放,則你需要修改bounds信息,確定縮放的策略,比如整體縮小一個zoom值,列出的公式大概會變成這個樣子[i*width*zoom,j*height*zoom,width*zoom,height*zoom]
僅僅說明一個方式不好,並不能證明其他的方式好,很多人已經想到了,我們可以把上面的這些“公式”抽離出來,整理出可復用的代碼。如果有其他的布局方案,也可以整理出對應的復用代碼,在paint之前應用上去,不就好了么?
對,其實,這個可復用的方案\策略,也就是“布局”。
以偽碼說明實現方式:
define Composite{ LayoutManager layoutManager; /** *繪制 */ void paint(){ layout(); //dopaint } /** *執行布局 */ void layout(){ layoutManager.layout(this); } } define LayoutManager{ void layout(Composite parent){ int index=0; //遍歷所有的children,獲取它們的layoutData,修改它們的位置 foreach(Control child:parent.getChildren()){ //根據child的index以及布局配置計算出偏移量 offset=computeOffset(index); //獲取child的布局數據 LayoutData data=child.getLayoutData(); //使用布局數據修改child的bounds信息 data.computeBounds(child,index,offset); index++; } } }
從上面偽碼我們可以看出,控件具備layoutData這個成員函數,容器(復合控件)具備layout這個成員函數.
layout用於規定該容器內部的布局類型(比如網格類型GridLayout),整體的布局規划(體現在上述代碼中的offset對象,為下一個需要布局的child提供偏移量)。
layoutData負責具體的某一個child在整體中的排布。
由於封裝在layout和layoutData中的都是算法(體現在computeXXX方法中),所以,我們可以靈活的規定、服用不同的組合方式,比如把一個控件布置在容器的整體居中位置。
完成了這些,你的UI框架就能用於展示各種視圖了。
3、事件分發
僅僅用於展示自然是不夠的,不然UI框架完全可以稱之為視圖框架,這個UI框架應當可以接收各種類型的鍵盤、鼠標輸入。
以H5的canvas為例,我們知道canvas是可以添加鼠標\鍵盤監聽的,你完成了控件和布局,點擊按鈕控件,響應事件的是按鈕還是canvas?
自然是canvas,瀏覽器哪里知道你寫了個“按鈕”出來。
對於不熟悉MVC模式的同學可能會有些疑惑,一個“繪制”上去的假按鈕,要如何響應事件呢?
我們再來看下這棵樹:
推論如下:
1、根容器的bounds等同於canvas的位置大小。
2、如果canvas接收到了鼠標事件,鼠標一定位於某個根容器下某個控件位置上。
3、樹遍歷控件,即可快速找到事件發生的時候,鼠標處於哪個控件之上。
鑒於JS在某些瀏覽器上的執行效率(我不是說微信),我們還可以做更多的優化。這里拋磚引玉:
1、引入層(layer)的概念,對樹結構再做一個橫向划分,優先查找層數高的控件,也可以讓某些層的控件不參與查找。
2、對控件本身設置可點擊屬性,畢竟if判斷的速度要比計算(x,y)是否位於bounds范圍內要快。
下一篇我來講講,如何制作游戲\動畫框架,再以后應該不會談UI相關的東西了,畢竟我也不是做這個的。