基礎知識漫談(2):從設計UI框架開始


說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相關的東西了,畢竟我也不是做這個的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM