原文:Creating Custom Layouts in Ext JS and Sencha Touch
布局系統是Sencha框架中最強大和最獨特的一部分。布局會處理應用程序中每個組件的大小和位置,因而,不須要手動去管理那些碎片。Ext JS與Sencha Touch的布局類有很多類似之處。近期在 Ivan Jouikov的這篇博文中對他們進行了具體的分析。
盡管是這樣。但非常多Ext JS和Sencha Touch開發者可能永遠都不會去了解布局系統的機制原理。Sencha框架已經提供了最經常使用的應用程序布局,因此非常少出現應用程序須要額外功能的需求,因而不大會有人愿意去了解布局系統的內部運作。
試想一下。你的公司須要在應用程序中使用3D Carousel來顯示界面元素。但沒有不論什么標準的Sencha布局能夠提供這樣的能力。哪怎么來解決問題呢?
選擇基類
在開發不論什么Ext JS或Sencha Touch自己定義組件的時候,第一步要考慮的總會是應該選擇哪一個基類來擴展。在眼下這樣的情況,就是須要使用哪種布局才干容納3D空間的項目。因為不須要不論什么特殊功能,僅僅是管理項目,因而,須要從布局繼承鏈最低這方面著手。在當前情況下,Ext.layout.container.Container (Ext JS)和Ext.layout.Default (Sencha Touch) 能夠說是最佳選擇。
在Ext JS中,因為須要支持舊版瀏覽器。而不能採用一些現代的CSS功能,如Flexbox,因而布局系統須要手動管理很多大小和定位的計算。這樣的后果就是,3D carousel布局須要重寫大多數的Ext.layout.container.Container方法,如calculate、getContainerSize和getPositionOffset。以便對它的子元素進行布局。
還有一個須要注意的問題是,Ext JS布局在執行“布局”時。每個“布局”的執行都要管理多個“周期”,比如。盒子布局配置為stretchmax就須要至少兩個周期,布局首先要檢測每個子組件的最大尺寸。并在第二周期將全部布局內的子組件擴展為同樣的大小。
布局的操作會產生大量的“布局”或“周期”(加入或移除多個條目),為了提供性能。可能會希望先暫停布局。在操作完畢后再恢復布局。
相比之下,Sencha Touch的Ext.layout.Default則同意瀏覽器通過CSS去處理布局中大多數項目的定位和尺寸(因為Sencha Touch僅僅支持現代瀏覽器,而全部這些都已實現CSS Flexbox)。
因此。Ext.layout.Default包括了與加入、移除和又一次定位子條目的相關的主要方法。
如今,已經了解將使用那些類來擴展新的3D Carousel布局。以下來逐步去實現它。
CSS3轉換和其它魔法
為了創建3D Carousel,須要使用一些高級CSS 3D轉換。如轉換、過渡、rotateX/rotateY和translateZ。CSS 3D轉換能夠非常復制。但總的來說,對于新的Sencha布局須要實現以下事情:
在父容器應用透視和轉換(讓它看上去是3D的)
在布局內的子組件應用轉換(環繞3D形狀從他們的邊界開始旋轉他們)
在父容器內部的DOM元素應用轉換(o physically rotate the 3D shape as the user interacts with it)
正如你所預期的,通過Ext JS和Sencha Touch生成的實際的DOM元素會有些許不同。因此。盡管在兩個框架中採取的方法是一樣的。但產生的CSS還是會有差別。新的3D Carousel布局所需的附加CSS例如以下(Sencha Touch):
.x-layout-carousel {-webkit-perspective : 1500px;-moz-perspective : 1500px;-o-perspective : 1500px;perspective : 1500px;position : relative !important;
}.x-layout-carousel .x-inner {-webkit-transform-style : preserve-3d;-moz-transform-style : preserve-3d;-o-transform-style : preserve-3d;transform-style : preserve-3d;
}.x-layout-carousel.panels-backface-invisible .x-layout-carousel-item {-webkit-backface-visibility : hidden;-moz-backface-visibility : hidden;-o-backface-visibility : hidden;backface-visibility : hidden;
}.x-layout-carousel-item {display : inline-block;position : absolute !important;
}.x-layout-carousel-ready .x-layout-carousel-item {-webkit-transition : opacity 1s, -webkit-transform 1s;-moz-transition : opacity 1s, -moz-transform 1s;-o-transition : opacity 1s, -o-transform 1s;transition : opacity 1s, transform 1s;
}.x-layout-carousel-ready .x-inner {-webkit-transition : -webkit-transform 1s;-moz-transition : -moz-transform 1s;-o-transition : -o-transform 1s;transition : transform 1s;
}
為了讓Ext JS布局的CSS看上去基本一樣,還須要對CSS選擇器做點微調。
為了在Sencha Touch和Ext JS中讓3D Carousel能響應用戶交互,不得不在執行時對一些附加的CSS的進行改動。以下首先要做的是擴展布局的基類,然后研究怎樣加入交互功能。
擴展布局基類
首先來擴展Sencha Touch的Ext.layout.Default,主要目標是加入一些針對新的3D Carousel的觀感的配置項,以及一些布局內部用來正確定位子組件的功能。
初步的擴展例如以下:
Ext.define('Ext.ux.layout.Carousel', {extend : 'Ext.layout.Default',alias : 'layout.carousel',config : {/*** @cfg {number} portalHeight* Height of the carousel, in pixels*/portalHeight : 0,/*** @cfg {number} portalWidth* Width of the carousel, in pixels*/portalWidth : 0,/*** @cfg {string} direction* 'horizontal' or 'vertical'*/direction : 'horizontal' //or 'vertical'},onItemAdd : function () {this.callParent(arguments);this.modifyItems();},onItemRemove : function () {this.callParent(arguments);this.modifyItems();},modifyItems : function () {//calculate child positions, etc}
});
代碼中除了config對象外,還定義了3個方法:onItemAdd、onItemRemove和modifyItems。
前啷個方法僅僅是簡單的重寫Ext.layout.Default的方法。以便在加入或刪除子組件后編輯子組件的位置,而modifyItems是一個新的方法。用來計算所需的CSS 3D轉換。
在布局指派給他們的容器時。布局系統內部的行為就會一直處于活動狀態:
setContainer: function(container) {var options = {delegate: '> component'};this.dockedItems = [];this.callSuper(arguments);container.on('centeredchange', 'onItemCenteredChange', this, options, 'before').on('floatingchange', 'onItemFloatingChange', this, options, 'before').on('dockedchange', 'onBeforeItemDockedChange', this, options, 'before').on('afterdockedchange', 'onAfterItemDockedChange', this, options);
},
對于我們的布局擴展來說。為了做進一步的初始化,還須要加上以下方法:
Ext.define('Ext.ux.layout.Carousel', {//...setContainer : function (container) {var me = this;me.callParent(arguments);me.rotation = 0;me.theta = 0;switch (Ext.browser.name) {case 'IE':me.transformProp = 'msTransform';break;case 'Firefox':me.transformProp = 'MozTransform';break;case 'Safari':case 'Chrome':me.transformProp = 'WebkitTransform';break;case 'Opera':me.transformProp = 'OTransform';break;default:me.transformProp = 'WebkitTransform';break;}me.container.addCls('x-layout-carousel');me.container.on('painted', me.onPaintHandler, me, { single : true });},onPaintHandler : function () {var me = this;//add the "ready" class to set the CSS transition stateme.container.addCls('x-layout-carousel-ready');//set the drag handler on the underlying DOMme.container.element.on({drag : 'onDrag',dragstart : 'onDragStart',dragend : 'onDragEnd',scope : me});me.modifyItems();}});
在nebulous指派布局容器后,必須等到容器渲染之后才干將事件處理指定究竟層的DOM。
接下來,為了能讓布局管理子組件。還要填補功能之間的縫隙( fill in the functional gaps):
Ext.define('Ext.ux.layout.Carousel', {//...modifyItems : function () {var me = this,isHorizontal = (me.getDirection().toLowerCase() === 'horizontal'),ct = me.container,panelCount = ct.items.getCount(),panelSize = ct.element.dom[ isHorizontal ? 'offsetWidth' : 'offsetHeight' ],i = 0,panel, angle;me.theta = 360 / panelCount;me.rotateFn = isHorizontal ? 'rotateY' : 'rotateX';me.radius = Math.round(( panelSize / 2) / Math.tan(Math.PI / panelCount));//for each child item in the layout...for (i; i < panelCount; i++) {panel = ct.items.getAt(i);angle = me.theta * i;panel.addCls('x-layout-carousel-item');// rotate panel, then push it out in 3D spacepanel.element.dom.style[ me.transformProp ] = me.rotateFn + '(' + angle + 'deg) translateZ(' + me.radius + 'px)';}// adjust rotation so panels are always flatme.rotation = Math.round(me.rotation / me.theta) * me.theta;me.transform();},transform : function () {var me = this,el = me.container.element,h = el.dom.offsetHeight,style= el.dom.style;// push the carousel back in 3D space, and rotate itel.down('.x-inner').dom.style[ me.transformProp ] = 'translateZ(-' + me.radius + 'px) ' + me.rotateFn + '(' + me.rotation + 'deg)';style.margin = parseInt(h / 2, 10) + 'px auto';style.height = me.getPortalHeight() + 'px';style.width = me.getPortalWidth() + 'px';},rotate : function (increment) {var me = this;me.rotation += me.theta * increment * -1;me.transform();}
});
在演示樣例中,還栩雅大量的復雜運算來確定每個子組件的正確位置,以及須要在容器內手動更新CSS的轉換值。
最后,還須要加入用來捕獲用戶與3D Carousel之間交互的事件處理:
Ext.define('Ext.ux.layout.Carousel', {//...onDragStart : function () {this.container.element.dom.style.webkitTransitionDuration = "0s";},onDrag : function (e) {var me = this,isHorizontal = (me.getDirection().toLowerCase() === 'horizontal'),delta;if (isHorizontal) {delta = -(e.deltaX - e.previousDeltaX) / me.getPortalWidth();}else {delta = (e.deltaY - e.previousDeltaY) / me.getPortalHeight();}me.rotate((delta * 10).toFixed());},onDragEnd : function () {this.container.element.dom.style.webkitTransitionDuration = "0.4s";}});
事件處理僅僅是簡單的評估用戶是否已將鼠標拖動到carousel,然后更新CSS過渡。
該類完整的Sencha Touch代碼能夠在這里下載。而擴展自Ext.layout.container.Container的Ext JS的代碼與這個非常類似,但在API上還是有一些小小的差別。
Ext JS代碼的演示樣例能夠在這里下載。
回想Ext.ux.layout.Carousel
以下化一點點時間來回想一下發生了什么事。
因為3D Carousel布局僅僅須要繼承布局系統的基本功能。因而選擇了Sencha Touch的Ext.layout.Default類進行擴展。接下來。加入了onItemAdd、onItemRemove和setContainer等重寫方法來加入布局所需的執行配置。最好,實現了一些功能方法和事件處理。以便布局能夠管理子組件的位置。
盡管3D Carousel是一個使用Sencha Touch或Ext JS創建的異想天開的樣例,但它的重點在于怎樣在Sencha 應用程序中創建有創意的布局,而這實際上非常easy。關鍵的地方是丫了解怎樣去初始化布局,以及在這期間發生了什么——其實底層的框架代碼并沒有你所想象的那樣復雜。Sencha Touch和Ext JS的布局系統。盡管在底層會有些許的不同,但實現方法是實際上是一樣的。
請注意:這僅僅是一個技術演示。不能保證代碼能執行在全部瀏覽器上。
而其實,所使用的CSS3轉換已經意味著排查了一些瀏覽器,因此請不要將這個應用到生產中。
Other interesting examples of customized layouts include this Sencha Fiddle by Sencha Support engineer Seth Lemmons involving a circle menu, and this video of a custom navigation component by Andrea Cammarata, Sencha Professional Services engineer.
作者:Arthur Kay
Arthur Kay is the Developer Relations Manager at Sencha, Inc. He studied Music and Computer Science at Loyola University Chicago and has been involved with the Web since the late 1990s.