Ext JS と Sencha Touch でカスタムレイアウトを生成する
こんにちは、ゼノフィnakamuraです。
レイアウトシステムは、Sencha フレームワークで最も強力でユニークな部分の一つです。レイアウトは、アプリケーションの各コンポーネントの位置とサイズを管理しますので、手動でそれらを管理する必要はありません。Ext JS と Sencha Touch のそれぞれのレイアウトクラスには似ているところが多くあり、弊社のアイバン・ジュイコフ (Ivan Jouikov) が最近この
ブログ記事
で細かく分析しました。
そうは言っても、ほとんどの Ext JS と Sencha Touch の開発者は、たぶんレイアウトシステムの内部を見た事がないと思います。Sencha フレームワークは、よくあるアプリケーションレイアウトのほとんどを提供していますから、アプリケーションに風変わりな機能が必要でない限り、レイアウトシステムの内部的な動作を検討することはないでしょう。
あなたの会社で 「3D カルーセル」 を使って、アプリケーションの UI 要素を表示する必要があると想像して下さい。Sencha の基本レイアウトではこの様な機能性を提供していません。さて、この課題にどう取り掛りましょうか?
ベースクラスの選択
Ext JS と Sencha Touch でカスタムコンポーネントを開発する時、最初のステップはどのベースクラスを拡張するのが最適か検討することです。
この場合、3D 空間にアイテムを配置するレイアウトが必要です。
そのため、アイテムの管理以外の機能性は必要ないので、レイアウトの継承チェーンの非常に低い位置から開始できます。
この場合では、
Ext.layout.container.Container
(Ext JS) と
Ext.layout.Default
(Sencha Touch) が最適でしょう。
その結果、我々の3Dカルーセルのレイアウトが子アイテムを配置するために Ext.layout.container.Container
の calculate()
, getContainerSize()
, getPositionOffset()
などのメソッドを上書きする必要があります。
これもまた重要なことですが、Ext JS レイアウトはレイアウトを “run” し、それぞれの run は複数の “cycles” を管理します。
例えば、stretchmax
で構成された ボックス レイアウトの場合は少なくとも二つの cycle が必要となります。レイアウトがまずそれぞれの子コンポーネントの最大のサイズを判断し、それから二つ目の cycle によって、レイアウトの全ての子アイテムを同じ大きさに引き伸ばします。
レイアウト run や cycle が大量に発生する操作 (例えば沢山のアイテムの追加や削除) では、パフォーマンスを改善するために、事前にレイアウトを中断して、操作が完了した後にレイアウトを再開させることができます。
一方、Sencha Touch の Ext.layout.Default
では、CSS によってレイアウトのほとんどのアイテムの位置やサイズをブラウザが管理できます。
(それは Sencha Touch は CSS Flexbox をサポートしているモダンなブラウザしかサポートしていないからです。)
そのため、Ext.layout.Default
に含まれているメソッドは、子アイテムの追加、削除、位置の変更に関するメソッドがほとんどです。
さて、我々の新しい「3D カルーセル」のレイアウトのために拡張するべきクラスが解りました。では実際に構築するために必要な手順を検討しましょう。
CSS3トランスフォームと他の素晴らしいもの
「3D カルーセル」を作るするためには、
transform
, transition
, rotateX/rotateY
, translateZ
などの高度な
CSS 3D トランスフォーム
を使う必要があります。
CSS 3D トランスフォームは、複雑な場合もありますが、要約すると、我々の新しい Sencha レイアウトの場合には、次の事を行う必要があります。
-
親コンテナに
perspective
とtransform
を適用する (3Dに見せるため) -
レイアウトの子コンポーネントに
transform
を適用する (それらを回転させて 3D シェイプの面にするため) -
親コンテナの内部の DOM エレメントに
transform
を適用する (ユーザーが操作したら 3D シェイプが実際に回転するため)
ご想像の通り、Ext JS と Sencha Touch が生成する実際の DOM は少しだけ違っていて、この記事で実施する手順は両方のフレームワークで同じですが、結果としての CSS は少々異なります。我々の新しい「3D カルーセル」にインクルーする必要がある追加の 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 カルーセル」の操作に反応するために、実行時に追加の CSS を変更する必要があります。まず、ベースレイアウトクラスを拡張してから、操作する機能の追加に取り組みましょう。
ベースレイアウトクラスの拡張
初めに Sencha Touch で Ext.layout.Default
を拡張して、「3D カルーセル」のルックアンドフィールに対するコンフィグオプションも追加し、このレイアウト内に子アイテムを正確に位置付けるユーティリティも追加します。
拡張した結果は、まずは次のようになります。
Ext.define('Ext.ux.layout.Carousel', { extend : 'Ext.layout.Default', alias : 'layout.carousel', config : { /** * @cfg {number} portalHeight * カルーセルの高さ - ピクセル値 */ portalHeight : 0, /** * @cfg {number} portalWidth * カルーセルの幅 - ピクセル値 */ portalWidth : 0, /** * @cfg {string} direction * 'horizontal' または 'vertical' */ direction : 'horizontal' //or 'vertical' }, onItemAdd : function () { this.callParent(arguments); this.modifyItems(); }, onItemRemove : function () { this.callParent(arguments); this.modifyItems(); }, modifyItems : function () { // 子アイテムの一を計算するなど } }); |
コンフィグオブジェクトの他に、三つのメソッドがあります。onItemAdd()
、onItemRemove()
、modifyItems()
です。
最初の二つのメソッドは、Ext.layout.Default
を単純にオーバーライドしていて、追加・削除の時点で子アイテムの位置を変更できるようになります。modifyItems()
は 3D トランスフォームを計算する新しいメソッドです。
レイアウトシステム内のアクションは、レイアウトクラスがコンテナ (Ext.layout.Default
から) を割り当てる時に動き出します:
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 state me.container.addCls('x-layout-carousel-ready'); //set the drag handler on the underlying DOM me.container.element.on({ drag : 'onDrag', dragstart : 'onDragStart', dragend : 'onDragEnd', scope : me }); me.modifyItems(); } }); |
内部的にレイアウトのコンテナを割り当てた後に、基盤となる DOM にイベントハンドラーを割り当てるために、コンテナが実際にレンダリングするまで待つ必要があります。次に、このレイアウトで管理された子アイテムをトランスフォームするために、機能的なギャップを埋る必要があります。
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 space panel.element.dom.style[ me.transformProp ] = me.rotateFn + '(' + angle + 'deg) translateZ(' + me.radius + 'px)'; } // adjust rotation so panels are always flat me.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 it el.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 カルーセル」を操作したこと補足するために、イベントハンドラーを追加する必要があります。
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"; } }); |
このイベントハンドラーは、ユーザーがカルーセルの上でマウスをドラッグした距離を判断し、CSS トランジションをアップデートします。
このクラスの完全な Sencha Touch コードはここにあります。Ext.layout.container.Container
を拡張する Ext JS のコードもとても似ていますが、フレームワークの間の違いに起因するちょっとした API の違いがあります。このサンプルの Ext JS コード は、ここにあります。
Ext.ux.layout.Carousel のレビュー
さて、ここまでで何が行われたか復習しましょう。
我々の新しい「3D カルーセル」レイアウトは、レイアウトシステムの基本的な機能を継承するだけでよかったため、Sencha Touch クラスの Ext.layout.Default
を拡張する事にしました。次に、レイアウトの実行時コンフィグにいくつかの追加を加えるために、onItemAdd()
、onItemRemove()
、setContainer()
に対するオーバーライドを追加しました。最後に、レイアウトの子アイテムの位置とサイズを管理するために、いくつかのユーティリティメソッドとイベントハンドラーを追加しました。
「3D カルーセル」のアイディアは、Sencha Touch または Ext JS で構築できる例として、気楽なサンプルですが、ここで示したい事は、Sencha のアプリケーションでクリエイティブで新しいレイアウトを作成するのは、本当に簡単だということです。重要な事は、レイアウトがどう初期化されているか理解する事と、実行時に何が行われるかを理解する事です。基盤となるフレームワークコードは、思ったより解り易いです。レイアウトシステムは、Sencha Touch と Ext JS の間で内部的な違いは少しありますが、全体的なアプローチは同じです。
ご注意下さい:これは、技術のデモで、私のコードは全てのブラウザで動作することは確保できません。CSS3 トランスフォームを利用している事だけで、既にいくつかの古いブラウザを除いた事になりますので、これを製品には利用しないで下さい。
他のカスタマイズしたレイアウトの良いサンプルをご紹介します。 Sencha 社のサポートエンジニアであるセス・レモンズ (Seth Lemmons) が開発したサークルメニューの Sencha Fiddle と、Sencha 社のプロフェッショナルサービスエンジニア、アンドレア・カマラータ (Andrea Cammarata) が作成したカスタムナビゲーションのビデオです。