Sencha Touch の小技-2 DataView を便利に!

  • 投稿:
  • 更新:2014年6月10日
  • by
  • in

Sencha Touch で一覧表とか作るのに割と出番の多い「Ext.DataView」
「Ext.data.Store」と組み合わせで利用するんですが、ちょっと細工すると今時のUIになります

リストを上下にスライドし上下の余白がある程度になったときに、上なら前ページ、下なら次ページのデータを読み込むインターフェースを実装してみます
データが複数のページに渡る場合、「次ページ」とか「前ページ」のボタンを配置しなくてよいのでモバイルな環境でのスペース節約になります


最初に実装の種明かしをすると、余白の高さをイベントハンドラでチェックし、ある程度(サンプルでは60ピクセル)に達したらフローティングウィンドウでメッセージを表示します

上記のサンプルは iOS Safari から試せます

http://jjworkshop.com/smf/S02/

ソースコード一式はこちらからダウンロードできます
サンプルでは次ページ前ページのデータ読み込みについては実装していません
UIの明示だけとなりますが、ページの管理処理は行ってますので、指定ページのデータ処理を組み込めばデータと連動します


コードを解説すると

まず、フローティングさせるメッセージパネルのCSS

 #bounceInfo.x-floating {
 padding: 8px;
 color: orange;
 font-size: 14px;
 line-height: 16px;
 text-align: center;
 background-color: #fff;
 border: 4px solid #bbb;
 -webkit-border-radius: 10px;
 -webkit-box-shadow: none;
 }

次は、Javascript のデータを処理する部分です
データはサンプルなのでハードコーディングしていますが、適当な「Ext.data.Proxy」からの派生クラスを用意すれば好きな場所からデータをもってこれます
この「Ext.regModel」(データモデル)「Ext.data.Store」(データストア)「Ext.XTemplate」(表示用テンプレート)の3点セットはデータを表示するときの御三家ですね(古っ…)

// データモデル
Ext.regModel('sampleData', {
 fields: ['loca', 'name', 'tel']
});
// データストア
var dataStore = new Ext.data.Store({
 model: 'sampleData',
 data : [
 {loca: '東京都', name: '山田' , tel: '080-1346-****'},
 {loca: '埼玉県', name: '佐藤' , tel: '090-2346-****'},
 {loca: '宮崎県', name: '後藤' , tel: '080-2346-****'},
 {loca: '大阪府', name: '権田' , tel: '070-9346-****'},
 {loca: '奈良市', name: '井崎' , tel: '020-2246-****'},
 {loca: '香川県', name: '薦田' , tel: '050-2546-****'},
 {loca: '山口県', name: '蓮田' , tel: '090-2846-****'},
 {loca: '大分県', name: '友夏' , tel: '080-2146-****'}
 ],
 autoLoad: true
});
// 表示用テンプレート(リスト表示の行データ)
var tpl = new Ext.XTemplate(
 '<tpl for=".">',
 '<div class="selecter">',
 '<table><tr><td><img src="./photo.png"/></td><td>{loca}</td></tr>',
 '<tr><td>{name}</td><td>{tel}</td></tr></table>',
 '</div>',
 '</tpl>',
 '<div class="x-clear"></div>'
);

最後に Javascript でデータを表示し、上下余白にUIを実装する部分です
ちょっと長めですが、主要部分の全てです
スワイプやダブルタップのハンドラもサンプルとして書いておきました

 // データリストビュー
 var listView = new Ext.DataView({
 fullscreen: true,
 autoHeight: true,
 itemSelector: 'div.selecter',
 emptyText: '<div class="notData">データがありません。</div>',
 singleSelect: true,
 store: dataStore,
 tpl: tpl,
 isEdit: false,
 listeners: {
 itemswipe: {
 // スワイプ:削除
 fn: function (view, idx, el, e) {
 Ext.Msg.alert(null, idx + "行スワイプ", Ext.emptyFn);
 }
 },
 itemdoubletap: {
 // ダブルタップ:
 fn: function (view, idx, el, e) {
 Ext.Msg.alert(null, idx + "行Wタップ", Ext.emptyFn);
 }
 }
 }
 });
 // データリスト表示パネル
 var listPanel = new Ext.Panel({
 fullscreen: true,
 monitorOrientation: true,
 nowPage: 1,
 newPage: -1,
 dockedItems: [listView],
 showBounceInfo: function (isShow, pos) {
 // 跳ねっ返りの文言を表示
 if (isShow)  {
 // 表示
 var msg = 'top' == pos ? '<p>▲ 前ページ</p>' : '<p>▼ 次ページ</p>';
 this.BounceInfoPos = pos;
 if (this.BounceInfoPanel == null)  {
 this.BounceInfoPanel = new Ext.Panel(
 {
 floating: true,
 id: 'bounceInfo',
 width: 250,
 height: 40,
 centered: true,
 html: msg,
 listeners: {
 show: {
 fn: function () {
 var pnSize = listPanel.getSize();
 var x = (pnSize.width - this.width) / 2;
 var y = 0;
 if (listPanel.BounceInfoPos == 'top')  {
 y = 10;
 }
 else {
 y = pnSize.height - this.height - 10;
 }
 this.setPosition(x , y);
 }
 }
 }
 });
 }
 this.BounceInfoPanel.update(msg);
 this.BounceInfoPanel.show();
 }
 else {
 // 消す
 if (this.BounceInfoPanel != null)  {
 this.BounceInfoPanel.hide();
 }
 }
 }
 });
 // リストビューの上下の跳ねっ返りを処理
 listView.scroller.on('offsetchange', function (obj, offset) {
 // オフセットを計算
 obj.pageMode = 0;
 if (offset.y>= 0) {
 if (offset.y> 60)  {
 // 上で跳ねっ返りON
 obj.pageMode = -1;
 listPanel.showBounceInfo(true, 'top');
 }
 else listPanel.showBounceInfo(false);
 }
 else {
 var delta = Math.abs(offset.y - listPanel.height) - obj.size.height;
 if (delta> 60)  {
 // 下で跳ねっ返りON
 obj.pageMode = 1;
 listPanel.showBounceInfo(true, 'bottm');
 }
 else listPanel.showBounceInfo(false);
 }
 });
 listView.scroller.on('bounceStart', function (obj) {
 // 跳ねっ返り開始
 if (obj.pageMode == -1 || obj.pageMode == 1)  {
 listPanel.newPage = listPanel.nowPage + obj.pageMode;
 }
 });
 listView.scroller.on('bounceEnd', function (obj) {
 // 跳ねっ返り終了(bounceStartで処理するとイベントが残るので、こっちで処理)
 listPanel.showBounceInfo(false);
 if (listPanel.newPage != -1)  {
 // このタイミングで前後ページデータを読み込む処理をする
 // (データ読み込み処理)
 console.log('Next page=' + listPanel.newPage);
 // 次に備える
 listPanel.nowPage = listPanel.newPage;
 listPanel.newPage != -1;
 }
 });

ポイントをざっと説明すると

3行目:fullscreen: true
これを忘れると何故かスクロールしてもリストが元にもどっちゃいます
(填りました…)

5行目:itemSelector: 'div.selecter'
テンプレート(Ext.XTemplate)で指定したトップレベルの DOM を指定しセレクターにします
(選択状態になるブロックです)

8行目:store: dataStore
データストアを指定します

9行目:tpl: tpl
表示用のテンプレートを指定します

スワイプやダブルタップのハンドラは見ての通りですね

31、32行目:nowPage: 1/newPage: -1
ページ管理用にクラス変数を追加しときます
nowPage=現在表示中ページ
newPage=次に表示するページ(-1は処理無し)

34行目:showBounceInfo メソッド
このメソッドは、上下の余白を表示したときのハンドラから呼ばれます
内部でフローティングパネルを表示しUIを明示します
パタンは3種類で
 ・上に「前ページ」を表示
 ・下に「次ページ」を表示
 ・消す
を引数により判定して処理してます
フローティングパネルは1つを使い回しし、インスタンスは「listPanel」にインプリメントしてます

44行目:id: 'bounceInfo'
ここの ID がポップアップ用(フローティングパネル)の CSS として定義

80行目:listView.scroller.on offsetchange ハンドラ
上下の余白が表示されるタイミングをを監視します
ここで指定範囲(サンプルでは60ピクセル)以上余白が出るとUIを明示するように「showBounceInfo」を呼びます
(またUI明示の必要なければ消すように指示)

101行目:listView.scroller.on bounceStart ハンドラ
跳ねっ返りが始まったとき(つまりスライドして指を放した直後)を監視します
ここで新しいページへの指示があったかチェックしてます
「offsetchange」ハンドラで規定値以上にスライドされていれば新しいページとなります

107行目:listView.scroller.on bounceEnd ハンドラ
跳ねっ返りが終わったとき(つまりリストの移動が落ち着いたとき)を監視します
実はここが肝でして、このタイミングでデータを読み込むように作らないと(サンプルでは読み込みはないですが)、イベントメッセージの残りが処理に干渉したりして面倒なんです

113行目:console.log('Next page=...
この部分は実際にはデータの読み込み処理になります
サンプルはコンソールへログを書くようになってますが、実際は「listPanel.newPage」ページ(0~n)を読み込むようにデータストアに指示することになります

と、まぁ こんな感じですね