Sencha Touchのカスタムコンポーネントを作る、パート1
こんにちは、ゼノフィkotsutsumiです。
開発者から我々のフレームワークについてもっと案内やチュートリアルが欲しいと聞いて、今日はSencha Touchのコンポーネントの作りの説明をします。ユーザーがオーディオのトラックのプレビューを聞いてその進行を丸いプログレスバーの中で表示するiOSのコンポーネントのようなHTML5のコンポーネントを作ってと言われました。その難題を受けて、たったの四時間でExt.tux.AudioCoverコンポーネントを作って、この3つに分かれているチュートリアルを書きました。このチュートリアルは自分のカスタムコンポーネント(「Touch User Extension」や「TUX」と知られている)の作り方をステップバイステップで説明します。
イタリア語が読めるのでしたら私の個人のサイトで元のポストを見られます。
始める前に考えること
最初のものは明確にあなたのカスタムコンポーネントはどう動くべきか、どのような機能を与えないといけないのかを明確に考えることです。この考えに基づけば、Sencha Touch標準コンポーネントのどれを拡張すべきか分かります。
Sencha TouchのフレームワークはContainers, Panels, Lists, Carouselsなどのようなデフォルトのコンポーネントの大きなセットがそろっていて、全ては違う機能や振る舞いが組み込まれています。 我々のExt.tux.AudioCoverコンポーネントが持つべき基本的な機能は次に挙げるような物ですので、そのベースとなるコンポーネントを選択します。
- オーディオファイルをプレーする(出来たらMP3)
- Play, pause, stopのようなベーシックなオーディオ機能を与える
- ながれているトラックの長さや今のプレータイムのような情報を与えること。
- そのユーザーインターフェイスをオーバーライド出来るようにする
Sencha Touchをすでにご存知の開発者であれば、必要なコンポーネントがExt.Audioだということはもうおわかりでしょう。 実はこのコンポーネントはオーディオファイルとその情報を全て管理できる機能をもっています。 その上、アプリケーションでのレンダリングに使われているHTMLテンプレートを簡単に操作できます。 このお陰でオーディオファイルのカバーに使われるイメージを見せられるようにUIを決めて、丸いプログレスバーをその時のオーディオトラックタイムを表示できます。
最初はグラフィカルな特徴と機能を強調しながら、UIの視点からネイティブのiOSコンポーネントを検討しましょう。
UIと機能のレビュー
ネイティブiOSコンポーネントをレビューすれば、 ユーザーインターフェースを作るのにどのHTMLノードが必要か、 このエレメントにどんなCSSのルールを適用すべきか、 同じ機能を確保する為にコンポーネントがどのイベントを扱うべきか よく理解できます。 さて、直接iOS6から撮られたスクリーンショットを見てみましょう:
最初の左の図で、プレイヤーは「アイドル」な状態になっていることが見える ー これはプレイリストが最初に描画される時か、表示されている再生中のトラックがない時です。 プレイヤーがこの状態になっている時は、リスト中のそれぞれのトラックはアルバムのカバーアートを表示しています。 ユーザーがトラックを「タップ」するとトラックのコンポーネントが関連するオーディオファイルを再生して、カバーをひっくり返して丸いプログレスバーと ユーザーが音声ファイルの再生を停止してオーディオのカバーイメージに戻すことができるストップボタンを表示します。
忘れてはいけないのはコンポーネントは一度に1トラックしかながせないということです。もしユーザーがあるトラックの再生中に別なトラックの再生を始めると、コンポーネントは「アイドル」の状況に戻ります。
チャレンジ
このチャレンジには二つの目的があります。
- HTMLエレメントの中に二つのdivをオーバーラップさせて、最初のはオーディオカバーを表示させて、二つ目には丸いプログレスバーを表示させます。
- 標準にはない丸いプログレスバーを作ること。
このチャレンジを一歩一歩進みましょう、最初はテンプレートの定義です。
テンプレートの定義
テンプレートはコンポーネントのスケルトンをとても簡単に定義できます。 実は、一つの単なる配列を定義するだけです。 配列の各要素には次の情報を設定します: どのような種類のHTMLノードが作られるべきか、 そこに適用するクラス、 もしあれば子エレメントの副配列、 生成したDOMノードを直接参照するコンポーネントの中のユニークな名前。
テンプレートの定義プロセスはとても簡単です。 次の節に表示されているコードを見てみましょう。
フリップ カード
始めにはフリップカードのテンプレートを定義しましょう。 これは我々のコンポーネントの二つの違う面をみせる部分です。 つまり1つ目の面は単なるイメージでオーディオのトラックカバーを表示して、もう一つの面は丸いプログレスバーを表示します。
2つ目の面はもう少し複雑なので、もう少し後で詳しく検討します; それまでの間は、プログレスバーには単に「Back」というプレースホルダーをつけておきましょう。心配しないでください、すぐに取り替えますから。
/** * @class Ext.tux.AudioCover * @extend Ext.Audio * @author Andrea Cammarata */ Ext.define('Ext.tux.AudioCover',{ extend: 'Ext.Audio', xtype: 'audiocover', template: [ { // The media element is required reference: 'media', preload: 'auto', tag: 'audio' }, { cls: 'x-flip-card', reference: 'cardEl', children: [ { cls: 'x-face x-front', reference: 'coverEl' }, { cls: 'x-face x-back', text: 'Back' } ] } ] });
tagフィールドが“audio”と設定されているノードに注目してください。 “Ext.Audio”コンポーネントを拡張しているのでこのノードは絶対に削除してはいけません。 Ext.Audioのコンポーネントの全てを書き直さないで、その代わりに既存の機能をうまく利用して我々のTUXのベースとして使います。 もし”audio”ノードを削除したら、 Ext.Audioコンポーネントに定義されている既存の関数が、 “this.media”によってaudioノードの参照を見つけないので、 コンポーネントが描画できなくなります。
“cardEl”エレメントを定義し、そこに二つの子を設定しています。 最初の子エレメントは表面に該当し、オーディオのトラックカバーを表示します。 二つめは裏面に結びつけられ丸いプログレスバーを表示します。
表面だけにreferenceを定義したことにお気づきでしょうか。 その理由は、 ユーザーがイメージを変えたい時にトラックカバーをアップデートする時に、 コンポーネントは表面のDOMノードの操作を扱うべきで、 裏面を直接扱う必要がないからです。
フリップカードのテンプレート定義をしました。一休みしてCSS側からこの部分をどう配置するか考えましょう:
“cardEl”エレメントの主な役割は二つの面のコンテナーだと考えましょう。 両方の面は”Flip”トランジションで回転します。 必要なCSSトランジションを二つの面の親のエレメント(すなわち”cardEl”)に与えることで両方が”Flip”トランジションで回転します。
大事なポイントは表面と裏面は並べずに、アブソリュートポジショニングを使用して、片方の面をもう片方の上にオーバーラップさせることです。 さらに、裏面(すなわち、下のレイヤー)はY軸で180度回転させます。 こうすると、”CardEl”全体が回されると裏面があらかじめ正しい向きになります。 これを真ん中で折った一枚の紙と考えても良いでしょう。 このすると、180度回転させる裏面に何が書(描)かれているか見えるようになります。
こうするのに必要なCSSルールは:
-webkit-transform: rotateY(180deg);
最後に、裏面を非表示にすることを覚えておかないと変なレンダリングの問題が発生します;これは次の様に解決できます:
-webkit-backface-visibility: hidden;
-webkit-transform-style: preserve-3d;
次の簡単なルールに従うと”cardEl”コンテナーの全ての子エレメントに 3D CSSトランスフォームするようになります。
プログレスバー
始める前にこの記事を読んだほうが良いでしょう。 これはとても興味深いCSSのルール「The Clip Rule」について知りたいことが全て説明されています。 簡単に言うとこのルールはimageやdivなどのの部分にマスクをかけられるようになります。 これは見通しの進化として考えれば良い:非表示のルールですが、エレメント全体を非表示にするのではなく一部分だけを表示します。
このルールをよく理解出来たら下のテンプレートを見てください。 これは丸いプログレスバーを完璧に描画し、その動きををシミュレートします:
template: [ { // The media element is required reference: 'media', preload: 'auto', tag: 'audio' }, { cls: 'x-flip-card', reference: 'cardEl', children: [ { cls: 'x-face x-front', reference: 'coverEl' }, { cls: 'x-face x-back', children: [ { cls: 'x-progress', reference: 'progressEl', children: [ { cls: 'x-slice x-half', children: [ { reference: 'slice1' } ] }, { cls: 'x-slice x-end', children: [ { reference: 'slice2' } ] } ] } ] } ] } ]
この場合では、 “border-radius”ルールを使って境界線を角丸にし、 そのコンテナーを面の中央に配置した、 “progressEl”コンテナノード を定義しています。 これでプログレスバーが狙い通りの丸い形になります。
しかし、これでもまだ足りません。 もう一度、上のtemplateを見ると二つ違うdivにベースCSSクラスの”x-slice”が定義されていることに気付きます。 双方にはreference名しかない子エレメントが追加されています。 これはこのコンポーネントがこの二つのHTML部分を操作することを意味します。
それでは、定義されているHTMLエレメントを使いながらどうやってプログレスバーのモーションをシミュレートできるのでしょうか? すべてのエレメントの配置を描いた次の図を見てください。 この図は最初の”x-slice”エレメント(追加の”x-half”クラスが設定されています)とその子エレメントだけを参照しています。
この図を見ると、x-face.x-backコンテナノード以外は全てのエレメントが同じような見た目、大きさ、位置だとすぐに気づきます。 しかし、ひとつだけ背景色がある丸いdivは、”x-slice.x-half”の子で、”slice1″として参照されます。 ほかのエレメントは全て塗り色が透明になっています。
コンポーネントの裏面にこのエレメントがどう位置されているか理解出来たので、また先へ進みましょう:
この四角には “x-slice.x-half”クラスとその子”slice1″のエレメントにどの様にCSS “clip”ルールが適用されているかを見ることができます。 灰色の部分は見えなくなるということを覚えておいてください。 白い部分だけが表示されます。 その為に、もし子供のエレメントが灰色の部分の下に位置すると、その子供のエレメントは完全に見えなくなります。 オーディオトラックの再生時間が0の時にプログレスバーがレンダリングされた時、 その開始位置は親HTML “x-slice.x-half”ノードの真下に位置しているので、 トラックの進行状況を表す青い部分は、完全に隠れています。
最終的にプログレスバーに息を吹き込む為に必要なのは、 コンポーネントテンプレートで定義した”media”エレメントの現在の時間を、 “slice”エレメントのZ軸の回転と同期を取ることです。 そうすると、毎回メディアの現在の時間がアップデートされる度に”slice1″エレメントの見える部分が親ノードの見える部分に入って、 図の③で示すように秒ごとにブルーの背景色の部分が大きくなって表示されます。
この時点一つだけ問題があります。 “Slice1″エレメントは180度までしか回転できません。さらに回転すると再び見えない部分に入り、我々のコンポーネントUIをめちゃくちゃにしてしまいます。 しかし、これは問題ではありません:オーディオトラックの現在時間が半分の位置まできたら、”slice1″エレメントは180度を回転しています。 そこで”slice2″エレメントを回転させて、このエレメントとその親の”x-slice.x-end”ノードが最初半分のノードの最初位置より真逆な位置におくと、 曲の後半部分の進行状況を表示できるようになります。
少し難しいコンセプトですが、このシリーズのパート2でまた説明します。次のブログポストではコンポーネントのギアを動かすJS関数について説明するので見てください。