HT圖形元件設計之道(二)

圖撲軟體發表於2014-08-13

上一篇我們自定義CPU和記憶體的展示介面效果,這篇我們將繼續採用HT完成一個新任務:實現一個能進行展開和合並切換動作的刀閘控制元件。對於電力SCADA和工業控制等領域的人機互動介面常需要預定義一堆的行業標準控制元件,以便使用者能做視覺化編輯器裡,通過拖拽方式快速搭建具體電力網路或工控環境的場景,並設定好裝置對應後臺編號等引數資訊,將拓撲圖形與圖元資訊一併儲存到後臺,實際執行環境中將開啟編輯好的網路拓撲圖資訊,連線後臺實時資料庫,接下來就是接受實時資料庫傳送過來的採集資訊進行介面實時動態重新整理,包括使用者通過客戶端對裝置進行的各種下發遙控等操作,傳送到後臺最終實現對硬體裝置的控制,這個過程就是典型的實時監控系統的基本架構流程。我們今天只做好小小螺絲釘工作,提供一個可控制的刀閘開關控制元件。

具體實現之前先看看我們要達到的最終效果圖片和視訊

Screen Shot 2014-08-12 at 9.14.50 PM

 

記得十多年前我剛畢業的第一份工作就是負責電力SCADA的人機介面互動模組,當時大部分電力行業都是採用VC/MFC或QT來實現介面呈現,其實至今也依然如此,前端時間和老朋友聚會了解到他們還在用VC6編譯系統,如今的VS20**根本跑不動他們龐大的古老系統,當然也許他們沒配置好工具引數,但從一個側面你可以感受到老系統遷移之重,大部分程式設計師處於為專案業務功能疲於奔命狀態,上百號人這麼多年在根本無力優化和重構的架子上不斷堆積功能,我記得當時一個mousedown函式居然堆了六千多行程式碼,各種圖元型別的draw程式碼也是長得不堪入目,這些老系統雖然不好維護但也考這麼多程式設計師活生生的維護下來了,我們每天能正常的用水用電用氣,背後都是靠著眾多程式設計師的血汗維護著以如今眼光看完全不堪入目的爛程式碼,不得不承認在中國能用是第一位,其他問題只要堆人能解決的都不是問題。有點扯遠了,上幾張我以前電力實現的相簿工具:

Screen Shot 2014-08-12 at 9.46.22 PM

實現功能並不難,當時也實現了組合和分解圖元,能進行相簿管理和使用者自定義,我相信全世界肯定不下幾百上千套繪圖軟體,剛開始我還是很興奮,每天學習不同的繪製API,就能搗鼓出新效果,我也不在乎程式碼架構,每天就是以學習掌握更多的龐大MFC庫為榮,但當你掌握大部分繪圖技巧後,我發現自己每天維護這種龐大到無法以個人力量進行大規模重構,又不得不持續維護每天堆積功能性體力活程式碼時,我感覺自己在浪費生命,於是跳槽到了另外一家公司打算做電子商務,結果陰差陽錯又被安排到電力部門幹起來繪圖工具,還好這次我能換個新語言Java,沒有歷史包袱完全自己重頭設計圖形架構,於是地球上出現了第1001個繪圖工具:

Screen Shot 2014-08-12 at 10.35.33 PM

這一版設計上還是有很大的改進,圖形繪製邏輯,互動程式碼以及介面佈局等都進行了較合理的分工設計,那個Java和設計模式很火,人手一本Martin Fowler《Refactoring: Improving the Design of Existing Code》,猶如宗教信仰堅決執行一個函式不超過幾十行的時代,一個mousedown幾千行的程式碼已經絕跡了,但我還是很不滿意,資料模型和介面繪製沒有很好的有機結合機制,雖然電力要求介面有***的毫秒級響應,但大部分公司都是像遊戲重新整理機制那樣不斷repaint介面,是的,當時的資料模型沒有任何事件派發機制,就是記憶體中的一堆資料,你無法知道哪個資料什麼時候change了,因而只能不斷的repaint介面,重新整理週期太短對於大的網路拓撲圖根本來不及更新,更新週期太長又達不到響應要求,至於所謂的***毫秒級響應我只能呵呵了,為了上這個系統一堆兄弟在瀋陽某農村封閉了八個多月,我很好奇那個老系統現在是否健在…

回到我們的任務,一個刀閘最主要的就是可開閉的部分,其他部分都是裝飾物效果而已,因此我採用HT的向量來描述整個刀閘外觀,其中需要開閉部分採用type為shape的一個線段來描述,並將其的rotation旋轉引數通過func: ‘style@switch.angle’的描述來繫結到Node圖元的switch.angle樣式屬性上

ht.Default.setImage('switch', {
    width: 100,
    height: 50,
    comps: [
        {
            type: 'roundRect',
            rect: [0, 0, 100, 50],
            background: '#2C3E50',
            gradient: 'linear.north'
        },
        {
            type: 'circle',
            rect: [10, 10, 10, 10],
            background: '#34495E',
            gradient: 'radial.center'
        },
        {
            type: 'circle',
            rect: [80, 10, 10, 10],
            background: '#34495E',
            gradient: 'radial.center'
        },
        {
            type: 'shape',
            points: [10, 40, 40, 40],
            borderWidth: 8,
            borderColor: '#40ACFF',
            border3d: true
        },
        {
            type: 'shape',
            points: [60, 40, 90, 40],
            borderWidth: 8,
            borderColor: '#40ACFF',
            border3d: true
        },
        {
            type: 'shape',
            points: [5, 40, 35, 40, 65, 40],
            segments: [1, 1, 2],
            borderWidth: 8,
            borderColor: '#40ACFF',
            border3d: true,
            borderCap: 'round',
            rotation: {
                value: -Math.PI/4,
                func: 'style@switch.angle'
            }
        },
        {
            type: 'circle',
            rect: [30, 35, 10, 10],
            borderColor: 'red',
            borderWidth: 5,
            border3d: true
        },
        {
            type: 'circle',
            rect: [60, 35, 10, 10],
            borderColor: 'red',
            borderWidth: 5,
            border3d: true
        }        
    ]
});

 

Screen Shot 2014-08-07 at 8.20.12 PM

以上是在向量編輯器中開啟的效果圖,你可以清晰的看得到我們定義的幾個元素的位置大小演示等,這樣應用時只要構建一個Node物件,將其image設定為switch向量,那麼將來只需要呼叫node.setStyle(‘switch.angle’, Math.PI/6)就可以隨時隨地控制刀閘展開角度 。

這樣封裝還不夠完美,對應用著來說他們只關心刀閘的開啟和關閉的操作,他們並不關心旋轉角度,開和關是業務角度的理解,而旋轉角度是底層實現圖形上的引數,並且使用者還需要開關過程有動畫效果,於是我們進行了進一步的封裝,設計了ht.Switch的類,提供了setExpanded的函式,在函式裡面操作底層繫結圖形的‘switch.angle’屬性,以及啟動動畫封裝:

ht.Switch = function(){    
    ht.Switch.superClass.constructor.call(this); 
    this.s('switch.angle', 0);
};
ht.Default.def('ht.Switch', ht.Node, {
    _image : 'switch',
    _icon: 'switch',

    toggle: function (anim) {
        this.setExpanded(!this.isExpanded(), anim);
    },
    isExpanded: function () {
        return this.s('switch.angle') !== 0;
    },
    setExpanded: function (expanded, anim) {
        if(anim == null){
            anim = true;
        }
        var self = this,
            animation = self._animation,
            oldValue = self.isExpanded();

        if(animation){
            animation.stop(true);
            delete self._animation;
        }

        if (oldValue !== expanded) {                        
            var targetAngle = expanded ? -Math.PI/4 : 0;                      

            if(anim){                
                oldValue = self.s('switch.angle');                
                self._animation = ht.Default.startAnim({
                    action: function(t){
                        self.s('switch.angle', oldValue + (targetAngle-oldValue)*t);
                    }
                });                                                  
            }else{
                self.s('switch.angle', targetAngle);
            }            
        }
    }
});
 在我們的視訊操作中你會發現通過屬性頁的拉條可以任意控制刀閘張角,同時通過isExpanded/setExpanded的boolean型別屬性也可以勾選動畫切換刀閘的開與關,細心的程式設計師你會發現不僅僅拓撲圖上的刀閘動起來了,連TreeView上的刀閘對應的icon圖示也是和向量描述的效果一樣,更驚喜的是樹上的icon也是實時顯示刀閘的展開角度,這是傳統圖片作為樹的icon圖片無法實現的,這也是我們一直強調的HT for Web整體架構已經為向量打下基礎,並非為了拓撲才實現向量,所有通用元件都享有向量的功能特性,這個後續我們會有更多的應用案例讓大家體會到這種結合的強大之處,當然可維護性已經不用我多說了,傳統的通用元件tree上自定義renderer也能實現一個能動的icon,但你可以想想工作量,我們沒有寫一行繪製程式碼,僅僅通過定義一個json的向量就把GraphView和TreeView的事都幹了,並且業務介面對上層應用人員來說就是一個node.setExpanded(true/false)之簡單。
 

這裡我只是隨手搞了個非常ugly的刀閘,你可以讓美工採用向量繪圖工具視覺化的繪製更漂亮的效果,介面操作上你也可以通過graphView.mi監聽互動事件,例如監聽到雙擊刀閘時進行開關切換,甚至可以參考《透過WebGL 3D看動畫Easing函式本質》的章節採用更洋相的Easing動畫效果。

最後幾點設計控制元件的建議:

  1. 切換到使用者角度,即站在上層應用者角度提供最簡潔符合業務邏輯的API介面,儘量不暴露圖形相關引數,圖形引數對上層使用著是晦澀的,暴露了你自己也是非常難改動和維護
  2. 不要一開始設計就考慮如何操作,如何動畫,操作和動畫都可以在基礎API基礎上擴充套件再封裝,某種程度上來說,如何操作和如何動畫甚至不屬於控制元件封裝該乾的,至少可再提供進一層的封裝,這樣可隨意切換操作和動畫邏輯,而不影響底層控制元件的資料模型和繪製邏輯
  3. 儘量讓繪製程式碼和業務邏輯程式碼分離,這點如果採用最基礎的繪製程式碼的確很難分離,這也是HT儘量採用向量描述,不讓使用者控制底層繪製程式碼的初衷

Screen Shot 2014-08-12 at 8.57.11 PM

相關文章