1. 程式人生 > >基礎知識漫談(2):從設計UI框架開始

基礎知識漫談(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範圍內要快。

特此宣告:因此文章個人認為總結的很到位,很有價值,特此轉載!轉載地址:http://www.cnblogs.com/anrainie/p/5609958.html